计算机网络(3) -- socket网络通信 Socket 是操作系统提供给应用程序的一套编程接口API是应用层 ↔ 传输层之间的桥梁封装了 TCP/IP 协议复杂的内核细节程序员不用直接操作网卡、IP 报文、TCP 首部调用函数就能实现网络通信。本质把网络通信抽象成文件 IOLinux 下一切 Socket 都是文件可用read / write 收发数据。Socket主要是实现跨主机进程之间的数据通信一台电脑上不同进程、不同电脑上的软件都可以依靠 Socket 收发数据。两大常用套接字类型有流式套接字SOCK_STREAMTCP和 数据报套接字 SOCK_DGRAMUDP。一、tcp服务器端与客户端编程流程TCP 提供的是面向连接的、可靠的、字节流服务。 TCP 的服务器端和客户端编程流程如下socket()方法是用来创建一个套接字有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型使用 TCP 协议选择流式服务SOCK_STREAM。bind()方法是用来指定套接字使用的IP 地址和端口。IP 地址就是自己主机的地址如果主机没有接入网络测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整型值一般 0-1024 为知名端口如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次 1024-4096 为保留端口 用户一般也不使用。 4096 以上为临时端口用户可以使用。在Linux 上 1024 以内的端口号只有 root 用户可以使用。在传参数的话需要先将IP地址和端口号转为网络字节序创建出sockaddr_in结构体在传参时进行强转。listen()方法是用来创建监听队列。监听队列有两种一个是存放未完成三次握手的连接(半连接队列)一种是存放已完成三次握手的连接(全连接队列)。 listen()第二个参数就是指定已完成三次握手队列的长度。accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。新建一个套接字描述符返回专门用来和对应客户端read/recv/write/send传输数据。如果该队列为空则 accept 阻塞。connect()方法一般由客户端程序执行需要指定连接的服务器端的 IP 地址和端口。该方法执行后会进行三次握手 建立连接。send()方法用来向 TCP 连接的对端发送数据。 send()执行成功只能说明将数据成功写入到发送端的发送缓冲区中并不能说明数据已经发送到了对端。 send()的返回值为实际写入到发送缓冲区中的数据长度。recv()方法用来接收 TCP 连接的对端发送来的数据。 recv()从本端的接收缓冲区中读取数据如果接收缓冲区中没有数据则 recv()方法会阻塞。返回值是实际读到的字节数如果recv()返回值为 0 说明对方已经关闭了 TCP 连接。下图演示了一方给另一方发送数据的过程因为TCP协议是双工通信的接收端同样也可以作为发送端它们都有自己独立的发送缓冲区和接收缓冲区。close()方法用来关闭 TCP 连接。此时会进行四次挥手。大端字节序和小端字节序当数据超过 1 个字节时比如 int 4 字节、short 2 字节 计算机在内存里存放这些字节的顺序就叫字节序。大端字节序高字节存低地址低字节存高地址像我们人类写字的顺序从左到右高位在前。小端字节序低字节存低地址高字节存高地址。0x1234 大端高字节存低地址低字节存高地址 低地址--高地址 0x12 34 小端低字节存低地址高字节存高地址 低地址--高地址 0x34 12可通过一个union联合体来观察是否电脑的字节序。#include stdio.h #include stdlib.h union Data { int i; char c; }; //判断电脑是大端还是小端 int main() { union Data data; data.i 0x12345678; if (data.c 0x78){ //高地址存高位所以是小端 printf(小端\n); }else if (data.c 0x12){ //高地址存低位所以是大端 printf(大端\n); } return 0; }通过编译运行发现我的电脑配置是小端一般像电脑手机这些大都是小端字节序而我们网络传输必须是大端所以我们需要通过一些系统调用来转换字节序。Linux 系统提供如下4 个函数来完成主机字节序和网络字节序之间的转换#include netinet/in.h l 管 IP、s 管端口发网络 h→n收数据 n→h //前两个传IP地址 uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序 参数主机序 32 位整型 IP 返回网络大端序IP赋值给sin_addr.s_addr uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序 参数从网络拿到的大端 32 位 IP 返回本机主机序IP整型 //后两个传端口号 uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序 参数主机十进制端口8080、6666 返回网络大端端口赋值给赋值 sin_port uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序 //参数报文里收到的网络序端口 //返回正常十进制端口用于打印IP 地址转换函数IP 地址转换函数通常我们习惯用点分十进制字符串表示 IPV4 地址但编程中我们需要先把它们转化为整数方能使用下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换#include arpa/inet.h in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序 char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示套接字结构体套接字的特性由3个属性确定,它们是:域(domain),类型(type)和协议(protocol);套接字用地址作为它的名字,地址的格式随域(又被称为协议族,protocol family)的不同而不同.每个协议族又可以使用一个或多个地址族来定义地址格式.通用socket地址结构,socket 网络编程接口中表示 socket 地址的是结构体 sockaddr其定义如下#include bits/socket.h struct sockaddr { sa_family_t sa_family;//协议族 char sa_data[14];//数据,没有给出IP地址,就是给了这么一块儿空间,起了一个占位的作用. };sa_family 成员是地址族类型sa_family_t 的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示但是一般我们不使用通用的sockaddr结构体进行传参我们一般使用TCP/IP 协议族中的sockaddr_in 和 sockaddr_in6两个专用socket 地址结构体它们分别用于IPV4 和 IPV6在传参数的时候必须强转为sockaddr * 类型。//sin_family: 地址族 AF_INET //sin_port: 端口号需要用网络字节序表示 //sin_addr: IPV4 地址结构 s_addr 以网络字节序表示 IPV4 地址 struct in_addr { u_int32_t s_addr;//无符号的32位的整型,存放IP地址; }; //tcp协议族 struct sockaddr_in { sa_family_t sin_family;//地址族,就是sin_family: 地址族 AF_INET u_int16_t sin_port;//端口,16位的端口 struct in_addr sin_addr;//一个结构体,只有一个成员,是无符号的32位的整型, 存放IP地址;(IPV4的地址就是32位) //其实后面还有占位的,只是我们不用它,所以就没有写; }; //tcp协议族就主要有三个:地址族,端口号,IP地址 //IP协议族 struct in6_addr { unsigned char sa_addr[16]; // IPV6 地址要用网络字节序表示 }; struct sockaddr_in6 { sa_family_t sin6_family; // 地址族 AF_INET6 u_inet16_t sin6_port; // 端口号用网络字节序表示 u_int32_t sin6_flowinfo; // 流信息应设置为 0 struct in6_addr sin6_addr; // IPV6 地址结构体 u_int32_t sin6_scope_id; // scope ID尚处于试验阶段 };前面了解了一些基础的转化接口和结构体下面为socket网络编程接口#include sys/types.h #include sys/socket.h int socket(int domain, int type, int protocol); //socket()创建套接字成功返回套接字的文件描述符失败返回-1 //domain: 设置套接字的协议簇 AF_UNIX AF_INET AF_INET6 //type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM // protocol: 一般设置为 0表示使用默认协议 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //bind()将 sockfd 与一个 socket 地址绑定成功返回 0失败返回-1 //sockfd 是网络套接字描述符,(命名套接字,就是上面的函数的返回值作为了我们的参数sockfd) //addr 是地址结构通过初始化sockaddr_in 之后强转成sockaddr * 类型 //addrlen 是 socket 地址的长度 int listen(int sockfd, int backlog); //listen()创建一个监听队列以存储待处理的客户连接成功返回 0失败返回-1 //sockfd 是被监听的 socket 套接字 //backlog 表示处于完全连接状态的 socket 的上限 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //accept()从 listen 监听队列中接收一个连接成功返回一个新的连接 socket //该 socket 唯一地标识了被接收的这个连接失败返回-1 //sockfd 是执行过 listen 系统调用的监听 socket //addr 参数用来获取被接受连接的远端 socket 地址 //addrlen 指定该 socket 地址的长度 int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); //connect()客户端需要通过此系统调用来主动与服务器建立连接 //成功返回 0失败返回-1 //sockfd 参数是由 socket()返回的一个 socket。 //serv_addr 是服务器监听的 socket 地址 //addrlen 则指定这个地址的长度 int close(int sockfd); //close()关闭一个连接实际上就是关闭该连接对应的 socket ssize_t recv(int sockfd, void *buff, size_t len, int flags); ssize_t send(int sockfd, const void *buff, size_t len, int flags); //TCP 数据读写 //recv()读取 sockfd 上的数据 buff 和 len 参数分别指定读缓冲区的位置和大小 //send()往 socket 上写入数据 buff 和 len 参数分别指定写缓冲区的位置和数据长度 //flags 参数为数据收发提供了额外的控制一般设置为0 //返回值如果为0,说明对方退出; //返回值如果为-1,表示出错;接下来我们实现两个程序一个是服务器端一个是客户端客户端写数据并发送给服务器端服务器端接收数据并返回ok。当客户端发送end时客户端退出连接。//ser.c #include stdio.h #include stdlib.h #include string.h #include arpa/inet.h //转换IP地址 #include netinet/in.h//转换字节序 #include sys/types.h #include sys/socket.h #include unistd.h //close函数 void AcceptClient(int sockfd) { while(1){ struct sockaddr_in client_addr; memset(client_addr, 0, sizeof(client_addr)); socklen_t client_addr_len sizeof(client_addr); int c accept(sockfd,(struct sockaddr *)client_addr, client_addr_len); //accept没有客户端连接时会阻塞 if(c 0){ perror(accept); continue; } char *client_ip inet_ntoa(client_addr.sin_addr); int client_port ntohs(client_addr.sin_port); printf(客户端连接成功IP: %s, 端口: %d\n, client_ip, client_port); while(1){ char buff[128] {0}; memset(buff, 0, sizeof(buff)); int n recv(c, buff, 127, 0); if(n 0){ perror(recv); break; } if(n 0){ printf(客户端已关闭连接\n); break; }else{ if(strncmp(buff, end, 3) 0){ close(c); break; } printf(收到客户端消息: %s\n, buff); send(c, ok, strlen(ok), 0); } } close(c); } } int main() { char *ip 192.168.199.128; int port 5000; int sockfd socket(AF_INET, SOCK_STREAM, 0); if(sockfd 0){ perror(socket); exit(1); } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(port); //转换端口号为网络字节序 server_addr.sin_addr.s_addr inet_addr(ip); //转换IP地址为网络字节序 socklen_t addr_len sizeof(server_addr); int res bind(sockfd, (struct sockaddr *)server_addr, addr_len); if(res 0){ perror(bind); exit(1); } printf(服务器绑定成功IP: %s, 端口: %d\n, ip, port); int backlog 5; res listen(sockfd, backlog); if(res 0){ perror(listen); exit(1); } printf(服务器已启动等待连接...\n); AcceptClient(sockfd); close(sockfd); return 0; }//cli.c #include stdio.h #include stdlib.h #include string.h #include arpa/inet.h #include netinet/in.h #include sys/types.h #include sys/socket.h #include unistd.h int main() { char * ip 192.168.199.128; int port 5000; int sockfd socket(AF_INET, SOCK_STREAM, 0); //创建套接字 if(sockfd 0){ perror(socket); exit(1); } struct sockaddr_in client_addr; memset(client_addr, 0, sizeof(client_addr)); client_addr.sin_family AF_INET; client_addr.sin_port htons(port); client_addr.sin_addr.s_addr inet_addr(ip); socklen_t addr_len sizeof(client_addr); int res connect(sockfd, (struct sockaddr *)client_addr, addr_len); //连接服务器 if(res 0){ perror(connect); exit(1); } printf(连接服务器成功IP: %s, 端口: %d\n, ip, port); while(1){ char sed[128] {0}; memset(sed, 0, sizeof(sed)); printf(input:); fgets(sed, 127, stdin); int slen strlen(sed); if(slen 0){ sed[slen - 1] 0; //去掉换行符 }else{ continue; } send(sockfd, sed, strlen(sed), 0); //发送数据 if(strncmp(sed, end, 3) 0){ break; } char buff[128] {0}; int n recv(sockfd, buff, 127, 0); //接收数据 if(n 0){ perror(recv); break; }else if(n 0){ printf(服务器已关闭连接\n); break; }else{ printf(收到服务器消息: %s\n, buff); } } close(sockfd); return 0; }编译运行结果如下我先将他在第二个终端上连接之后在连接第三个终端上的cli程序但是在服务器端它是不能做到并行处理的它只能一个客户端处理完成断开连接后去处理下一个客户端因为在代码逻辑上它是一个终端进入while1循环执行读写操作遇到end后退出循环断开本次连接后才回去accept下一个连接它是串行处理的串行处理就会导致服务器运行效率低下。当前服务器采用单线程阻塞 I/O 模型属于串行处理 同一时间只能服务一个客户端必须等待当前客户端断开连接后服务器才能回到 accept 接收下一个客户端连接。 这种模型无法并行处理多个客户端在多用户访问场景下会导致服务效率低下、响应延迟。要实现多客户端并行处理必须引入多进程或多线程并发处理多进程每接入一个客户端fork 一个子进程专门处理通信多线程每接入一个客户端创建一个线程专门处理通信两者都能让服务器同时服务多个客户端互不阻塞。下面我先写出服务器端多进程的处理逻辑代码。在多进程处理中我又单独封装了一个函数用于处理接收到的客户端数据。fork的时机应该放在accept之后它从全连接队列中取得一个连接子进程用于处理客户端数据父进程则继续从accept中拿去连接。//这是ser的代码用上一次的cli.c可以直接验证 #include stdio.h #include stdlib.h #include string.h #include arpa/inet.h //转换IP地址 #include netinet/in.h//转换字节序 #include sys/types.h #include sys/socket.h #include unistd.h //close函数 void DealOneClient(int c) //将处理客户端数据单独封装成一个函数 { while(1){ char buff[128] {0}; memset(buff, 0, sizeof(buff)); int n recv(c, buff, 127, 0); if(n 0){ perror(recv); break; } if(n 0){ printf(客户端已关闭连接\n); break; }else{ if(strncmp(buff, end, 3) 0){ close(c); break; } printf(客户端%d: %s\n,getpid(), buff); send(c, ok, strlen(ok), 0); } } close(c); printf(%d 已经断开连接\n,getpid()); } void AcceptClient(int sockfd) { while(1){ struct sockaddr_in client_addr; memset(client_addr, 0, sizeof(client_addr)); socklen_t client_addr_len sizeof(client_addr); int c accept(sockfd,(struct sockaddr *)client_addr, client_addr_len); //在accept之后进行fork父进程继续监听子进程处理客户端数据 if(c 0){ perror(accept); continue; } char *client_ip inet_ntoa(client_addr.sin_addr); int client_port ntohs(client_addr.sin_port); printf(客户端连接成功IP: %s, 端口: %d\n, client_ip, client_port); int res fork(); //fork函数返回两次父进程返回子进程的PID子进程返回0 if(res 0){ perror(fork); close(c); continue; }else if(res 0){ DealOneClient(c); } } } int main() { char *ip 192.168.199.128; int port 5000; int sockfd socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字返回一个文件描述符 if(sockfd 0){ perror(socket); exit(1); } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(port); //转换端口号为网络字节序 server_addr.sin_addr.s_addr inet_addr(ip); //转换IP地址为网络字节序 socklen_t addr_len sizeof(server_addr); int res bind(sockfd, (struct sockaddr *)server_addr, addr_len); //绑定套接字到指定的IP地址和端口号 if(res 0){ perror(bind); exit(1); } printf(服务器绑定成功IP: %s, 端口: %d\n, ip, port); int backlog 5; res listen(sockfd, backlog); //将套接字设置为监听状态等待客户端连接backlog参数指定了连接请求队列的最大长度 if(res 0){ perror(listen); exit(1); } printf(服务器已启动等待连接...\n); AcceptClient(sockfd); //接受客户端连接并处理数据 close(sockfd); return 0; }我同过打开两个终端来测试是否可以同时处理两个客户端连接。这是不难看出客户端是可以支持并发处理多个客户端程序了。下面是多线程处理处理的方法同样也是相同的逻辑在accept之后创建出线程之后线程去执行处理客户端数据主线程继续从全连接队列拿取数据但是这里有一个需要注意的主线程不能调用pthread_join函数因为它会等待函数线程执行完成后在继续执行如果函数线程执行过慢则客户端连接会有延迟出现。所以这里我们介绍一个函数pthread_detath它是用来分离函数线程并自动回收系统资源的但是它并不会阻塞住主线程所以需要确保主线程不会先退出如果先退出了必须在主线程最后调用pthread_exit(NULL);让其他线程运行完在结束进程在结束但是不推荐这种做法。#include pthread.h // 成功返回 0失败返回错误号 int pthread_detach(pthread_t thread); //功能分离状态的线程退出后系统会自动回收它的资源不需要其他线程调用 pthread_join 来等待回收。 //thread:要设置为分离状态的线程 ID#include stdio.h #include stdlib.h #include string.h #include arpa/inet.h //转换IP地址 #include netinet/in.h//转换字节序 #include sys/types.h #include sys/socket.h #include unistd.h //close函数 #include pthread.h void* DealOneClient(void *arg) //将处理客户端数据单独封装成一个函数 { int c *(int *)arg; while(1){ char buff[128] {0}; memset(buff, 0, sizeof(buff)); int n recv(c, buff, 127, 0); if(n 0){ perror(recv); break; } if(n 0){ printf(客户端已关闭连接\n); break; }else{ if(strncmp(buff, end, 3) 0){ close(c); break; } printf(客户端%d: %s\n,c, buff); send(c, ok, strlen(ok), 0); } } close(c); printf(%d 已经断开连接\n,c); return NULL; } void AcceptClient(int sockfd) { while(1){ struct sockaddr_in client_addr; memset(client_addr, 0, sizeof(client_addr)); socklen_t client_addr_len sizeof(client_addr); int c accept(sockfd,(struct sockaddr *)client_addr, client_addr_len); //在accept之后进行fork父进程继续监听子进程处理客户端数据 if(c 0){ perror(accept); continue; } char *client_ip inet_ntoa(client_addr.sin_addr); int client_port ntohs(client_addr.sin_port); printf(客户端连接成功IP: %s, 端口: %d\n, client_ip, client_port); pthread_t id; int res pthread_create(id, NULL, DealOneClient, (void *)c); //创建线程处理客户端数据 if(res ! 0){ perror(pthread_create); close(c); } pthread_detach(id); //分离线程线程结束后自动释放资源 } } int main() { char *ip 192.168.199.128; int port 5000; int sockfd socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字返回一个文件描述符 if(sockfd 0){ perror(socket); exit(1); } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(port); //转换端口号为网络字节序 server_addr.sin_addr.s_addr inet_addr(ip); //转换IP地址为网络字节序 socklen_t addr_len sizeof(server_addr); int res bind(sockfd, (struct sockaddr *)server_addr, addr_len); //绑定套接字到指定的IP地址和端口号 if(res 0){ perror(bind); exit(1); } printf(服务器绑定成功: %s, 端口: %d\n, ip, port); int backlog 5; res listen(sockfd, backlog); //将套接字设置为监听状态等待客户端连接backlog参数指定了连接请求队列的最大长度 if(res 0){ perror(listen); exit(1); } printf(服务器已启动等待连接...\n); AcceptClient(sockfd); //接受客户端连接并处理数据 close(sockfd); return 0; }编译运行结果显示同样也可以实现多客户端并发执行。二、UDP服务器端与客户端编程流程因为UDP协议是无连接的,不可靠的,数据报服务;所以它不需要listen来创建全连接队列和半连接队列所以也没有accept来拿取全连接队列的描述符。故它的连接示意图如下socket()用来创建套接字使用 udp 协议时选择数据报服务 SOCK_DGRAM。sendto()用来发送数据由于UDP 是无连接的每次发送数据都需要指定对端的地址(IP 和端口); recvfrom()接收数据每次都需要传给该方法一个地址结构来存放发送端的地址。recvfrom()可以接收所有客户端发送给当前应用程序的数据并不是只能接收某一个客户端的数据。ssize_t recvfrom( int sockfd, // 1. 套接字文件描述符 void *buff, // 2. 接收数据的缓冲区 size_t len, // 3. 缓冲区最大长度 int flags, // 4. 接收标志一般填 0 struct sockaddr* src_addr, // 5. 【输出】发送方的地址IP端口 socklen_t *addrlen // 6. 地址长度输入输出参数 ); 返回值 0成功返回实际收到的字节数 0对方关闭连接 -1出错用 errno 查看错误 ssize_t sendto( int sockfd, // 1. 套接字文件描述符 void *buff, // 2. 要发送的数据 size_t len, // 3. 数据长度 int flags, // 4. 发送标志一般填 0 struct sockaddr* dest_addr, // 5. 【输入】目标地址IP端口 socklen_t addrlen // 6. 地址长度 ); 返回值 0成功返回实际发送的字节数 -1发送失败同样我们通过udp服务来实现服务器接收来自客户端从终端上读取的数据但因为udp是无连接的所以即使没有多进程或者多线程它同样也可以同时接收来自不同客户端的数据并发的执行处理逻辑。//udp_ser.c #include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #include sys/socket.h #include netinet/in.h #include sys/types.h int main() { int sockfd socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字 if (sockfd -1) { perror(socket); exit(0); } struct sockaddr_in ser_addr; memset(ser_addr, 0, sizeof(ser_addr)); ser_addr.sin_family AF_INET; ser_addr.sin_port htons(5000); ser_addr.sin_addr.s_addr inet_addr(192.168.199.128); socklen_t ser_len sizeof(ser_addr); int res bind(sockfd, (struct sockaddr *)ser_addr, ser_len); //绑定套接字到指定的IP地址和端口号 if(res -1) { perror(bind); exit(0); } char buf[1024]; while (1){ memset(buf, 0, sizeof(buf)); struct sockaddr_in cli_addr; socklen_t cli_len sizeof(cli_addr); ssize_t recv_len recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)cli_addr, cli_len); //接收客户端发送的消息 if (recv_len -1) { perror(recvfrom); break; }else if(recv_len 0) { printf(客户端已关闭连接\n); break; }else{ printf(客户端IP%s:%d- %s\n, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf); sendto(sockfd, ok, 2, 0, (struct sockaddr *)cli_addr, cli_len); //回复客户端消息 } } close(sockfd); //关闭套接字 return 0; }//udp_cli.c #include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #include sys/socket.h #include netinet/in.h #include sys/types.h int main() { int sockfd socket(AF_INET, SOCK_DGRAM, 0); if (sockfd -1) { perror(socket); exit(EXIT_FAILURE); } struct sockaddr_in ser_addr; memset(ser_addr, 0, sizeof(ser_addr)); ser_addr.sin_family AF_INET; ser_addr.sin_port htons(5000); ser_addr.sin_addr.s_addr inet_addr(192.168.199.128); socklen_t ser_len sizeof(ser_addr); char buf[1024]; while (1){ memset(buf, 0, sizeof(buf)); printf(请输入要发送的消息: ); fgets(buf, sizeof(buf), stdin); int size strlen(buf); if(size 2)continue; buf[size - 1] \0; ssize_t send_len sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)ser_addr, ser_len); if (send_len -1) { perror(sendto); break; } if(strncmp(buf, end, 3) 0){ break; } // struct sockaddr_in rcv_addr; // socklen_t rcv_len sizeof(rcv_addr); // memset(rcv_addr, 0, sizeof(rcv_addr)); // ssize_t recv_len recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)rcv_addr, rcv_len); // if(recv_len -1) // { // perror(recvfrom); // break; // }else if(recv_len 0) // { // printf(服务器已关闭连接\n); // break; // }else{ // printf(服务器%s:%d- %s\n, inet_ntoa(rcv_addr.sin_addr), ntohs(rcv_addr.sin_port), buf); // } } close(sockfd); return 0; }编译运行结果如下下图也显示出它可以同时接收来自不同客户端的数据。当我们将客户端的recvform注释防止其阻塞不然客户端会阻塞在这里不能在发送三个你数据并且将服务器端的recvfrom中的接收大小设置为2则会发现服务器端每次只接受2个字节大小的数据其他数据都被丢弃了这也体现udp传输的数据报服务以及不可靠的特点。而且在服务器关闭状态下发送数据还是可以发送的这体现出它们无连接不可靠的特点发送出的数据被直接丢弃掉了重启服务器并不会接收未启动时客户端发来的数据。通过以上代码与实验我们已经掌握了TCP 的多进程 / 多线程并发模型以及UDP 的无连接并发模型的基础用法。但在实际高并发场景中多进程 / 多线程模型的资源开销与调度瓶颈会被放大而 UDP 虽然天然无连接但也无法解决 “如何高效同时监听多个文件描述符” 的问题。无论是 TCP 还是 UDP当服务器需要同时处理大量客户端连接 / 数据报时传统的 “阻塞 I/O 多进程 / 多线程” 模型会遇到明显的性能瓶颈进程 / 线程的创建与上下文切换开销巨大大量空闲连接会占用系统资源单线程阻塞 I/O 无法同时处理多个文件描述符。为了解决这些问题Linux 系统提供了I/O 多路复用技术核心实现包括select poll 和 epoll。这些机制允许单进程 / 单线程同时监听多个文件描述符仅在有数据就绪时才进行处理大幅提升了服务器的并发处理能力与资源利用率。这些内容我会在之后的文章中详细讲述。