⭐️在这个怀疑的年代我们依然需要信仰。个人主页YYYing.⭐️Linux/C进阶系列专栏【从零开始的linux/c进阶编程】系列上期内容【Linux/C多线程篇(二) 】同步互斥机制 C 11下的多线程系列下期内容【Linux/C网络篇(二) 】TCP并发服务从多进程到Epoll的进化指南目录引言程序如何“联网”网络编程基本概念一、字节序二、IP地址IP地址的分类特殊的IP地址点分十进制三、端口号端口号的分类网络编程基础一、套接字socket的概念二、基于TCP面向连接的通信方式 bind函数 listen函数 accept函数 recv、send数据收发 close关闭套接字 connect连接函数三、TCP的C/S模型服务端代码实现客户端代码实现四、基于UDP面向无连接的通信方式 recvfrom、sendto数据收发五、UDP的C/S模型服务端代码实现客户端代码实现结语---⭐️封面自取⭐️---引言程序如何“联网”你有没有想过你写的 C/C 程序是怎么跟千里之外的服务器交换数据的聊天软件、网页浏览、在线游戏……这些应用程序背后都依赖着网络编程。而网络编程的核心就是Socket套接字——它就像程序打开的一扇“网络窗户”数据从这扇窗户进出。那么我们现在就从零开始通过代码实战彻底搞懂网络通信的核心模型。网络编程基本概念关于TCP和UDP的讲解我们计网篇章的传输层部分讲的内容已经足够了我们本篇章的基础概念主要是围绕着字节序、ip和端口号来展开一、字节序字节序计算机在存储多字节整数时根据主机的CPU处理架构不同我们将主机分成大端存 储的主机和小端存储的主机大端存储内存地址低位存储的是数据的高位小端存储内存地址的低位存储的是数据的低位我们不妨可以来验证一下我们主机是大端存储还是小端存储使用指针来判断#include iostream int main(int argc, const char *argv[]) { //定义一个整形变量 int num 0x12345678; //定义一个字符类型的指针指向整形变量的起始地址 char *ptr (char *)num; //对ptr所指向的字节中的内容进行判断如果是0x12则说明是大端存储 //如果是0x78则说明是小端存储 if(*ptr 0x12){ coutbig endianendl; } else if(*ptr 0x78){ cout little endianendl; } std::cout Hello, World! std::endl; return 0; }使用共用体来判断#include iostream #include cstdio using namespace std; //定义一个共用体类型多个成员共享一个成员的空间共享的是所占内存空间最大的那个成员 union Info{ int num; //四字节整数 char ch; //一字节 }; int main(int argc, const char *argv[]) { //定义一个共用体变量 union Info temp; //给其整形成员赋值 temp.num 0x12345678; //判断其ch成员 if(temp.ch 0x12){ coutbig endianendl; }else if(temp.ch 0x78){ coutlittle endianendl; } std::cout Hello, World! std::endl; return 0; }而由于不同主机之间存储方式不同可能会出现小端存储的主机中的多字节整数在网络传输过程中明明没有出现任何问题但是由于大小端存储问题导致多字节整数传输出现错误。基于此我们引入的网络字节序的概念规定网络字节序都是大端存储的。无论发送端是大端存储还是小端存储在传输多字节整数时一律先转换为网络字节序。经由网络传输后到达目的主机后在转换为主机字节序即可。系统给大家提供了一套有关网络字节序和主机字节序之间相互转换的函数主机host网络network转换to#include arpa/inet.h //将4字节整数主机字节序转换为网络字节序参数是主机字节序返回值是网络字节序 uint32_t htonl(uint32_t hostlong); //将2字节整数主机字节序转换为网络字节序参数是主机字节序返回值是网络字节序 uint16_t htons(uint16_t hostshort); //将4字节整数的网络字节序转换为主机字节序参数是网络字节序返回值是主机字节序 uint32_t ntohl(uint32_t netlong); //将2字节整数的网络字节序转换为主机字节序参数是网络字节序返回值是主机字节序 uint16_t ntohs(uint16_t netshort);那么我们何时使用网络字节序转换函数呢在进行多字节整数网络传输时需要使用字节序转换函数最典型的就是我们的端口号在进行单字节整数传输时不需要使用在网络中传输字符串时也不需要使用二、IP地址此处在我们之前计网篇章也讲过了但此处我们再提及下个别概念ip地址是主机在网络中的唯一标识由两部分组成分别是网络号和主机号。网络号确定计算机所从属的网络主机号标识该设备在该网络中的一个编号在网络传输过程中给网络传输载体必须添加的信息指定源ip地址和目的ip地址以便于找到目的主机IP地址的分类IPv4是使用4字节无符号整数表示的一个ip地址取值范围 [0, 2^32-1] 一共有四十多亿个很明显不够我们采用相关技术进行扩充局域网扩充为了解决ip地址不够用让多个主机共享一个ip地址 WANwide area network 广域网 LANlocal area network局域网IPv6是使用16字节无符号整数表示的一个ip地址取值范围 【0 2^128-1】注意IPv6是不兼容IPv4的。网络类型取值范围网络号个数主机号个数用途A类网络1.0.0.0 --- 127.255.255.2552^72^24已经保留不供给使用B类网络128.0.0.0 --- 191.255.255.2552^142^16名地址网管中心C类网络192.0.0.0 --- 223.255.255.2552^212^8家庭、校园、公司使用D类网络224.0.0.0 --- 239.255.255.255------组播IPE类网络240.0.0.0 --- 255.255.255.255------保留、实验室使用特殊的IP地址1、网络号 全为0的主机号表示该网络不分配给任何主机使用例如192.168.10.02、网络号 全为1的主机号表示当前网络的广播地址也不分配给任何主机使用例如 192.168.10.2553、网络号 主机号为1默认表示网关当然可以自己制定网关ip4、127.0.0.0本地环回ip当没有网络时用于测试当前主机的ip5、0.0.0.0表示当前局域网中的任意一个主机号6、255.255.255.255一般表示广播地址点分十进制为了方便记忆我们将ip地址的每一个字节单独计算出十进制数据并用点进行分割这种方式称为点分十进制在程序中使用的是字符串来存储的。但是ip地址的本质是4字节无符号整数在网络中进行传输时需要使用的是4字节无符号整数而不是点分十进制的字符串。此时就需要引入关于点分十进制数据向4字节无符号整数转换的相关函数地址address网络network转换to#include sys/socket.h #include netinet/in.h #include arpa/inet.h //将点分十进制的ip地址转换为4字节无符号整数的网络字节序 //参数时点分十进制数据返回值时4字节无符号整数 in_addr_t inet_addr(const char *cp); //将4字节无符号整数的网络字节序转换为点分十进制的字符串 char *inet_ntoa(struct in_addr in);三、端口号端口号port是一个 2 字节的无符号整数表示的数字取值范围 【0 65535】为了区分同一个主机之间的每个进程的使用端口号来进行标识为什么不使用进程号标识而使用端口号?答因为进程号是进程的唯一标识当同一个应用程序关闭再打开后并不是同一个进程号了但是是同一个应用程序所以端口号标识的是我们的应用程序当一个应用程序关闭再打开后端口号不变引入端口号后网络通信的两个重要因素就集结完毕ip 地址 端口号ip地址可以在网络中唯一确定对端的主机地址通过端口号能够找到该主机中指定的对端应用程序端口号的分类1、0~1023 众所周知的 “VIP”端口号被特殊的应用程序已经占用了的。2、1024 ~ 49151用户可分配的端口号3、49152~65535动态分配或系统自动分配的端口号网络编程基础一、套接字socket的概念Socket 是操作系统提供的一个接口它封装了底层网络细节。程序员通过 Socket 函数创建、绑定、连接、发送、接收数据就像操作文件一样方便。我们可以调用函数socket创建一个用于通信的套接字端点并返回该端点对应的文件描述符在通信端点中有两个缓冲区分别对应发送缓冲区和接受缓冲区函数原型int socket(int domain, int type, int protocol);头文件sys/types.hsys/socket.h功能为通信创建一个端点并返回该端点对应的文件描述符文件描述符的使用原则是最小未分配 原则参数说明参数1协议族常用的协议族如下AF_UNIX, AF_LOCAL 本地通信同一主机的多进程通信AF_INET 提供IPv4的相关通信方式AF_INET6 提供IPv6的相关通信方式具体可以看man 7 ip参数2通信类型指定通信语义常用的通信类型如下SOCK_STREAM 支持TCP面向连接的通信协议SOCK_DGRAM 支持UDP面向无连接的通信协议参数3通信协议当参数2中明确指定特定协议时参数3可以设置为0但是有多个协议共同使用时需要用参数3指定当前套接字确定的协议返回值成功返回创建的端点对应的文件描述符失败返回-1并置位错误码二、基于TCP面向连接的通信方式在网络通信过程中有两种通信方式分别是基于BS模型的即浏览器服务器模型和基于CS模型即客户端服务器模型我们本篇章使用的是基于CS模型那么通信原理就是下图值得一提的是我们此处服务器用完listen启动监听状态并不是我们服务器自身在等而是相当于我们的服务器招了一位经理这位经理来帮我们处理发来的连接请求且这位经理还有着队列结构当有人发来连接后我们的经理就会看此时服务器有没有人在连接中如果有就将其放在队列中直到服务器的对端关闭了后再进行接受连接。造成这一点的主要原因是上面的示例是单线程阻塞模型同一时间只能服务一个客户端此处的 TCP 示例中无法同时处理多个新连接需要配合多进程/多线程/IO多路复用。你可以把这一套操作想象成电话机创建 Socket 买一部电话机Bind 给电话机分配一个号码端口Listen/Accept 等待别人打进来Connect 主动拨打别人的号码Send/Recv 通话内容 bind函数函数原型int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);头文件sys/types.hsys/socket.h功能为套接字分配名称给套接字绑定ip地址和端口号参数说明参数1要被绑定的套接字文件描述符参数2通用地址信息结构体对于不同的通信域而言使用的实际结构体是不同的该结构体的目 的是为了强制类型转换防止警告通信域为AF_INET而言ipv4的通信方式struct sockaddr_in {sa_family_t sin_family; /* 地址族: AF_INET */in_port_t sin_port; /* 端口号的网络字节序 */struct in_addr sin_addr; /* 网络地址 */};/* Internet address. */struct in_addr {uint32_t s_addr; /* ip地址的网络字节序 */};通信域为AF_UNIX而言本地通信struct sockaddr_un {sa_family_t sun_family; /* 通信域AF_UNIX */char sun_path[UNIX_PATH_MAX]; /* 通信使用的文件 */};参数3参数2的大小返回值成功返回0失败返回-1并置位错误码 listen函数函数原型int listen(int sockfd, int backlog);头文件sys/types.hsys/socket.h功能将套接字设置成被动监听状态参数说明参数1套接字文件描述符参数2挂起队列能够增长的最大长度一般为128返回值成功返回0失败返回-1并置位错误码 accept函数函数原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);头文件sys/types.hsys/socket.h功能阻塞等待客户端的连接请求如果已连接队列中有客户端则从连接队列中拿取第一个并创 建一个用于通信的套接字参数说明参数1服务器套接字文件描述符参数2通用地址信息结构体用于接受已连接的客户端套接字地址信息的参数3接收参数2的大小返回值成功发那会一个新的用于通信的套接字文件描述符失败返回-1并置位错误码 recv、send数据收发函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t send(int sockfd, const void *buf, size_t len, int flags);头文件sys/types.hsys/socket.hsys/types.hsys/socket.h功能从套接字中读取消息放入到buf中向套接字文件描述符中将buf这个容器中的内容写入参数说明参数1通信的套接字文件描述符参数2要存放数据的起始地址参数3读取的数据的大小参数4读取标识位是否阻塞读取0表示阻塞等待MSG_DONTWAIT非阻塞参数1通信的套接字文件描述符参数2要发送的数据的起始地址参数3发送的数据的大小参数4发送标识位是否阻塞发送0表示阻塞等待MSG_DONTWAIT非阻塞返回值可以是大于0表示成功读取的字节个数可以是等于0表示对端已经下线针对于TCP通信失败返回-1并置位错误码成功返回发送字节的个数失败返回-1并置位错误码 close关闭套接字函数原型int close(int fd);头文件unistd.h功能关闭套接字文件描述符参数说明要关闭的套接字文件描述符返回值成功返回0失败返回-1并置位错误码 connect连接函数函数原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);头文件sys/types.hsys/socket.h功能将指定的套接字连接到给定的地址上参数说明参数1要连接的套接字文件描述符参数2通用地址信息结构体参数3参数2的大小返回值成功返回0失败返回-1并置位错误码三、TCP的C/S模型服务端代码实现#includeiostream #includecstdio #includecstring #include sys/types.h #include unistd.h #include netinet/in.h #include arpa/inet.h #include sys/socket.h #define SER_PORT 8888 #define SER_IP 192.168.160.129 int main(int argc, const char *argv[]){ // 1、创建用于连接的套接字文件描述符 int sfd socket(AF_INET, SOCK_STREAM, 0); if(sfd -1){ perror(socket error); return -1; } printf(socket success sfd %d\n, sfd); // 2、绑定ip地址与端口号 struct sockaddr_in sin; sin.sin_family AF_INET; // 此处绑定数字都为网络字节序 sin.sin_port htons(SER_PORT); sin.sin_addr.s_addr inet_addr(SER_IP); if(bind(sfd, (struct sockaddr*)sin, sizeof(sin)) -1){ perror(bind error); return -1; } printf(bind success\n); // 3、启动监听 if(listen(sfd, 128) -1){ perror(listen error); return -1; } printf(listen success\n); // 4、阻塞等待客户端的连接请求 struct sockaddr_in cin; socklen_t socklen sizeof(cin); int newfd accept(sfd, (struct sockaddr*)cin, socklen); if(newfd -1){ perror(accept error); return -1; } printf([%s:%d]:已连接成功!!!\n, inet_ntoa(cin.sin_addr), ntohs(cin.sin_port)); // 5、数据收发 char rbuf[128] ; while(1){ bzero(rbuf, sizeof(rbuf)); int res recv(newfd, rbuf, sizeof(rbuf), 0); if(res 0){ printf(对端已下线\n); break; } printf([%s:%d]:%s\n, inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf); strcat(rbuf, *_*); if(send(newfd, rbuf, strlen(rbuf), 0) -1){ perror(send error); return -1; } printf(send success\n); } // 6、关闭套接字 close(newfd); close(sfd); return 0; }客户端代码实现#includeiostream #includecstdio #includecstring #include sys/types.h #include unistd.h #include netinet/in.h #include arpa/inet.h #include sys/socket.h using namespace std; #define SER_PORT 8888 #define SER_IP 192.168.160.129 #define CLI_PORT 9999 #define CLI_IP 192.168.160.129 int main(int argc, const char *argv[]){ // 1、创建用于通信的客户端套接字文件描述符 int cfd socket(AF_INET, SOCK_STREAM, 0); if(cfd -1){ perror(socket error); return -1; } printf(socket success cfd %d\n, cfd); // 3 // 2、绑定ip与端口号(可选) struct sockaddr_in cin; cin.sin_family AF_INET; cin.sin_port htons(CLI_PORT); cin.sin_addr.s_addr inet_addr(CLI_IP); if(bind(cfd, (struct sockaddr*)cin, sizeof(cin)) -1){ perror(bind error); return -1; } printf(绑定成功\n); // 3、链接服务器 struct sockaddr_in sin; sin.sin_family AF_INET; sin.sin_port htons(SER_PORT); sin.sin_addr.s_addr inet_addr(SER_IP); if(connect(cfd, (struct sockaddr*)sin, sizeof(sin)) -1){ perror(connect error); return -1; } printf(连接服务器成功\n); // 4、数据收发 char wbuf[128] ; while(1){ bzero(wbuf, sizeof(wbuf)); fgets(wbuf, sizeof(wbuf), stdin); wbuf[strlen(wbuf) - 1] 0; // 将换行改掉 if(send(cfd, wbuf, sizeof(wbuf), 0) -1){ perror(send error); return -1; } if(recv(cfd, wbuf, sizeof(wbuf), 0) 0){ printf(对端已下线\n); break; } printf(收到服务器消息为%s\n,wbuf); } // 5、关闭套接字 close(cfd); return 0; }四、基于UDP面向无连接的通信方式udp通信是面向无连接的不可靠的尽最大努力传输的通信方式传输过程中可能会出现数据的丢失、重复、失序、乱序等现象创建通信套接字是使用的传输层名称为SOCK_DGRAM此处不难发现我们两种模型的函数其实也就只有数据的收发不一样 recvfrom、sendto数据收发函数原型ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);头文件sys/types.hsys/socket.hsys/types.hsys/socket.h功能从套接字中读取消息放入到buf中并接受对端的地址信息结构体向套接字中发送消息并且指定对方的地址信息结构体参数说明参数1套接字文件描述符参数2存放数据的容器起始地址参数3读取的数据大小参数4是否阻塞0表示阻塞MSG_DONTWAIT表示非阻塞参数5接收对端地址信息结构体的容器参数6参数5的大小参数1套接字文件描述符参数2要发送的数据的起始地址参数3要发送的数据大小参数4是否阻塞0表示阻塞MSG_DONTWAIT表示非阻塞参数5要发送的对端地址信息结构体参数6参数5的大小返回值成功返回读取字节的个数失败返回-1并置位错误码成功返回发送的字节的个数失败返回-1并置位错误码五、UDP的C/S模型服务端代码实现#includeiostream #includecstdio #includecstring #include sys/types.h #include unistd.h #include netinet/in.h #include arpa/inet.h #include sys/socket.h using namespace std; #define SER_PORT 8888 #define SER_IP 192.168.160.129 int main(int argc, const char *argv[]){ // 1、创建用于连接的套接字文件描述符 int sfd socket(AF_INET, SOCK_DGRAM, 0); if(sfd -1){ perror(socket error); return -1; } printf(socket success sfd %d\n, sfd); // 2、绑定ip地址与端口号 struct sockaddr_in sin; sin.sin_family AF_INET; // 此处绑定数字都为网络字节序 sin.sin_port htons(SER_PORT); sin.sin_addr.s_addr inet_addr(SER_IP); if(bind(sfd, (struct sockaddr*)sin, sizeof(sin)) -1){ perror(bind error); return -1; } printf(bind success\n); // 3、数据收发 struct sockaddr_in cin; socklen_t socklen sizeof(cin); char rbuf[128] ; while(1){ bzero(rbuf, sizeof(rbuf)); if(recvfrom(sfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)cin, socklen) -1){ perror(recvfrom error); break; } printf([%s:%d]:%s\n, inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf); strcat(rbuf, *_*); sendto(sfd, rbuf, strlen(rbuf), 0, (struct sockaddr*)cin, sizeof(cin)); printf(send success\n); } // 4、关闭套接字 close(sfd); return 0; }客户端代码实现#includeiostream #includecstdio #includecstring #include sys/types.h #include unistd.h #include netinet/in.h #include arpa/inet.h #include sys/socket.h using namespace std; #define SER_PORT 8888 #define SER_IP 192.168.160.129 #define CLI_PORT 9999 #define CLI_IP 192.168.160.129 int main(int argc, const char *argv[]){ // 1、创建用于连接的套接字文件描述符 int cfd socket(AF_INET, SOCK_DGRAM, 0); if(cfd -1){ perror(socket error); return -1; } printf(socket success sfd %d\n, cfd); // 2、绑定ip地址与端口号 struct sockaddr_in cin; cin.sin_family AF_INET; cin.sin_port htons(CLI_PORT); cin.sin_addr.s_addr inet_addr(CLI_IP); if(bind(cfd, (struct sockaddr*)cin, sizeof(cin)) -1){ perror(bind error); return -1; } printf(绑定成功\n); // 3、数据收发 struct sockaddr_in sin; sin.sin_family AF_INET; sin.sin_port htons(SER_PORT); sin.sin_addr.s_addr inet_addr(SER_IP); char wbuf[128] ; while(1){ bzero(wbuf, sizeof(wbuf)); fgets(wbuf, sizeof(wbuf), stdin); wbuf[strlen(wbuf) - 1] 0; // 将换行改成换行 sendto(cfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)sin, sizeof(sin)); printf(send success\n); recvfrom(cfd, wbuf, sizeof(wbuf), 0, NULL, NULL); printf(服务器发来的消息为%s\n, wbuf); } // 4、关闭套接字 close(cfd); return 0; }结语网络编程其实并不神秘核心就是创建 Socket - 绑定/连接 - 收发数据 - 关闭。TCP像打电话建立连接后放心聊适合重要数据。UDP像寄信扔进邮筒就不管了适合实时性要求高的场景。掌握了这些基础 API你就拿到了通往高性能服务器开发的大门钥匙但我们目前并不能让我们的TCP服务器实现多台客户端的连接那么下一步我将带着大家进入TCP并发服务器的学习。我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见---⭐️封面自取⭐️---