Linux——socket网络编程
一、主机字节序列和网络字节序列一主机字节序CPU在内存中进行数据存取的顺序。二进制存在高位和低位之分左边是高位右边是低位内存存储存在高地址和低地址之分一块空间起始处总是低地址一个存储单元大于1字节的数据就会存在字节序的问题大端字节序一个整数的高位字节存储在内存的低地址处低位字节存储在内存的高地址处高地址存低位低地址存高位例如0x01020304 - 01 02 03 04小端字节序一个整数的高位字节存储在内存的高地址处低位字节存储在内存的低地址处高地址存高位低地址存低位例如0x01020304 - 04 03 02 01字节序的决定性CPU架构X86小端字节序MIPS大端字节序二网络字节序在两台使用不同字节序的主机之间传递数据时可能会出现冲突所以在将数据发送到网络时规定整形数据使用大端字节序所以也把大端字节序称为网络字节序。对方接收到数据后可以根据自己的字节序进行转换三字节序转换接口字节序转换针对的数据类型shortintlongfloatdouble...大于1字节的类型1从主机字节序转换为网络字节序2字节数据的转换uint16_thtons(uint16_t val);常用于转换端口4字节数据的转换uint32_thtonl(uint32_t val);常用于转换IP地址注意短整型数据的转换不能使用 htonl2从网络字节序转换为主机字节序2字节数据的转换uint16_tntohs(uint16_t val);常用于转换端口4字节数据的转换uint32_tntohl(uint32_t val);常用于转换IP地址3点分十进制的转换IP地址本质就是一个4字节的无符号整数用ntohlhtonl进行转换即可但是为了使IP地址能够更加便于记忆会采用点分十进制字符串进行表示例如192.168.12.12所以很多时候IP地址的转换并不是转换实际的数字而是一个字符串。将字符串点分十进制IPv4地址转换为网络字节序数字IPv4地址struct in_addr inet_addr(const char *ipstr);将网络字节序数字IPv4地址转换为字符串点分十进制IPv4地址const char* inet_ntoa(sruct in_addr addr); // 非线程安全接口将网络字节序整数IP地址转换为字符串IP地址可转换IPv4或IPv6const char* inet_ntop(int domain, void *src, char *dst, size_t size); /* domain描述要转换的地址类型AF_INETipv4或 AF_INET6ipv6 src网络字节序的数据地址 dst转换后的字符串存放空间地址 sizedst空间的大小 */将字符串的IP地址转换为网络字节序IP地址可转换为IPv4或IPv6int inet_pton(int domain, const char *src, void *dst);二、套接字编程一网络通信网络通信也是进程间通信方式的一种相较于以前的进程间通信网络通信可以实现跨主机通信网络通信也是进程间通信方式但是通信两端有不同的角色客户端主动向服务器发送请求获取服务的一端通常部署在客户机上服务端通常部署在网络服务提供商的服务器上接收客户端请求并提供服务通信分为两种不同的架构C/S客户端服务器模式(client/server)B/S浏览器服务器模式(browser/server)二套接字地址结构套接字一套网络通信API接口1.通用 socket 地址结构socket 网络编程接口中表示 socket 地址的是结构体 sockaddrstruct sockaddr { sa_family_t sa_family; char sa_data[14]; };sa_family 成员是地址族类型sa_family_t的变量。地址族类型通常与协议族类型对应常见的协议族和对应的地址族如下2.专用 socket 地址结构TCP/IP协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体它们分别用于IPv4和IPv6struct in_addr { u_int32_t s_addr; }; struct sockaddr_in { sa_family_t sin_family; // 地址族 AF_INET u_int16_t sin_port; // 端口号 用网络字节序表示 struct in_addr sin_addr; // IVP4地址结构 s_addr以网络字节序表示IPV4地址 }; struct in6_addr { unsigned char sa_addr[16]; //IPV6地址 用网络字节序表示 }; struct sockaddr_in6 { sa_family_t sin6_family; // 地址族 AF_INET u_inet16_t sin6_port; // 端口号 用网络字节序表示 u_int32_t sin6_flowinfo; // 流信息 应设置为0 struct in6_addr sin6_addr; // IVP6地址结构 u_int32_t sin6_scope_id; // scope ID };三、网络编程接口一创建套接字在内核中创建网络通信描述信息并获取套接字的文件描述符int socket (int domain, int type, int protocol); /* domain:地址域描述通信地址类型 AF_UNIX:域间通信(本地通信)struct sockaddr_un AF_INET:IPV4通信struct sockaddr_in AF_INET6:IPV6通信struct sockaddr_in6 type:设置套接字的服务类型 SOCK_STREAM:流式套接字有序的可靠的双向的基于字节流 SOCK_DGRAM:数据报套接字无连接的不可靠的有最大长度限制的 SOCK_RAW:提供更底层原始的数据包捕获 protocol:协议类型选择要使用的协议 默认值 0SOCK_STREAM是tcp协议SOCK_DGRAM是udp协议 IPPROTO_TCPTCP协议 IPPROTO_UDPUDP协议 返回值: 成功则返回套接字的文件描述符非负整数 失败返回 -1 */二为套接字绑定地址信息将sockfd与一个socket地址绑定作用告诉网卡发送数据的时候数据的源端地址信息是什么当前设备网卡的IP地址端口号告知操作系统网卡接收数据的时候哪些数据应该交给当前套接字进行处理int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen); /* sockfd:套接字描述符socket返回的描述符 addr:要绑定的地址信息 addrlen地址信息长度 返回值成功返回 0失败返回 -1 */三连接服务器用于TCP协议客户端客户端通过此系统调用来主动与服务器建立连接int connect(int sockfd,const struct sockaddr *serv_addr, socklen_t addrlen); /* serv_addr:要连接的服务端地址信息 addrlen:指定这个地址的长度 返回值:成功返回 0失败返回 -1 */四开始监听用于TCP协议服务端创建一个监听队列以存储待处理的客户连接int listen(int sockfd,int backlog); /* sockfd:被监听的socket套接字 backlog:同一时间能够接受最大客户端连接请求数量 返回值:成功返回 0失败返回 -1 */五获取就绪连接用于TCP协议服务端从listen监听队列中接收一个连接返回一个新的连接socket该socket唯一地标识了被接收地这个连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); /* sockfd:执行过listen系统调用的监听socket addr:获取呗接受连接的远端socket地址 addrlen:既要设置要获取多长的地址并且还要返回实际的地址信息长度 返回值:成功则返回指定客户端通信句柄描述符;失败返回 -1 */六接收数据接收数据的接口是一个阻塞接口若没有数据则等待1.TCP的接收接口读取sockfd上的数据ssize_t recv(int sockfd, void *buff, size_t len, int flags) /* sockfd:对于客户端来说就是socket的返回值对于服务端来说是accept的返回值 buff:指定读缓冲区的位置 len:指定读缓冲区的大小 flags:为数据接收提供了额外的控制 通常默认为 0表示阻塞接收 MSG_DONTWAIT:本次recv能拿到数据则直接返回没有数据则报错返回 MSG_PEEK:接收数据但是不从缓冲区删除数据常用于检测性接收 返回值:成功则返回实际读取到的数据长度出错则返回-1TCP通信连接断开返回0 */2.UDP的接收接口读取sockfd上的数据ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) /* buff:指定读缓冲区的位置 len:指定读缓冲区的大小 src_addr:记录发送端的socket地址 addrlen:指定该地址的长度 返回值:成功则返回实际读取到的数据长度出错则返回-1 */七发送数据发送数据的接口是一个阻塞接口发送过快导致数据将socket发送缓冲区塞满了就会阻塞1.TCP的发送接口往socket上写入数据ssize_t send(int sockfd, const void *buff, size_t len, int flags); /* buff:指定写缓冲区的位置 len:指定写缓冲区的数据长度 flags为数据发送提供了额外的控制 通常默认为0表示缓冲区满的时候函数阻塞等待缓冲区腾出空间 返回值:实际发送的数据长度; 出错则返回-1; */2.UDP的发送接口往socket上写入数据ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen); /* buff:指定写缓冲区的位置 len:指定写缓冲区的数据长度 dest_addr:指定接收数据端的socket地址 addrlen:指定该地址的长度 返回值:实际发送的数据长度; 出错则返回-1 */八关闭套接字关闭一个连接实际上就是关闭该连接对应的socketint close(int sockfd);四、UDP编程流程UDP提供的是无连接、不可靠的、数据报服务一编程流程sokcet()方法是用来创建一个套接字有了套接字就可以通过网络进行数据的收发创建套接字时要指定使用的服务类型使用udp协议选择数据报服务SOKC_DGRAMbind()方法是用来指定套接字使用的IP地址和端口IP地址就是自己主机的地址如果主机没有接入网络测试程序时可以使用回环地址“127.0.0.1”。端口是一个16位的整形值一般0-1024为知名端口这类端口一般用户不能随便使用1024-4096为保留端口用户一般也不使用4096以上为临时端口用户可以使用在Linux上1024以内的端口号只有root用户可以使用sendto()用来发送数据由于UDP是无连接的每次发送数据都需要指定对端的地址IP和端口recvfrom()接收数据每次都需要传给该方法一个地址结构来存放发送端的地址recvfrom()可以接收所有客户端发送给当前应用程序的数据并不是只能接收某一个客户端的数据close()关闭连接二代码编写1.服务端/* 编写一个简单的UDP通信服务端程序 功能模拟简单的聊天程序 1.接收客户端消息进行展示 2.从标准输入获取一个信息响应给客户端 */ #include stdio.h #include string.h #include unistd.h // close #include sys/socket.h // socket api接口 #include netinet/in.h // 地址结构和协议宏声明 #include arpa/inet.h // 字节序转换的api接口 int main() { // 1.创建套接字 // int socket(int domain, int type, int protocol); // AF_INETIPv4通信SOCK_DGRAM数据报套接字 // 协议类型也可以给0表示UDP因为SOCK_DGRAM数据报套接字默认协议就是UDP int sockfd socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd 0) { perror(创建套接字失败); return -1; } // 1.5 构造地址信息结构填充要绑定的地址信息 // 绑定的IP地址必须是当前一有的网卡IP地址绑定哪个网卡则接收的就是哪个网卡收到的数据 struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9090); // 要转换为网络字节序 // in_addr_t inet_addr(const char *__cp) // int inet_pton(int af, const char *src, void *dst); // 方法一 // addr.sin_addr.s_addr inet_addr(192.168.182.128); // 方法二 inet_pton(AF_INET, 192.168.182.128, addr.sin_addr); // 2.为套接字绑定地址信息 // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(sockfd, (struct sockaddr *)addr, addrlen); if (ret 0) { perror(绑定地址信息失败); return -1; } while (1) { // 3.接受客户端数据 // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); struct sockaddr_in peerAddr; socklen_t addrlen sizeof(struct sockaddr_in); char buffer[1024] {0}; // 用于存放获取出来的数据 ret recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)peerAddr, addrlen); if (ret 0) { perror(recvfrom接收数据失败); continue; } printf(客户端[%s:%d] say:%s\n, inet_ntoa(peerAddr.sin_addr), ntohs(peerAddr.sin_port), buffer); // 4.捕获键盘输入 memset(buffer, 0x00, 1024); printf(服务器 say:); fflush(stdout); scanf(%s, buffer); // 5.向客户端响应数据 // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ret sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)peerAddr, addrlen); if (ret 0) { perror(sendto 发送数据失败); continue; } } // 6.关闭套接字 close(sockfd); return 0; }2.客户端/* 编写一个简单的UDP通信客户端程序 功能模拟一个简单的聊天功能 1.从标准输入捕获一个字符串发送给服务端 2.等待服务端的响应接收后展示出来 */ #include stdio.h #include string.h #include unistd.h // close #include sys/socket.h // socket api接口 #include netinet/in.h // 地址结构和协议宏声明 #include arpa/inet.h // 字节序转换的api接口 #define CHECH_RESULT(ret, msg) \ if ((ret) 0) \ { \ perror(msg); \ return -1; \ } int main() { // 1.创建套接字 int sockfd socket(AF_INET, SOCK_DGRAM, 0); CHECH_RESULT(sockfd, 创建套接字失败); // 2.为套接字绑定地址信息大多时候不需要主动绑定 // 2.5 构造地址信息结构 struct sockaddr_in srvAddr; srvAddr.sin_family AF_INET; srvAddr.sin_port htons(9090); srvAddr.sin_addr.s_addr inet_addr(192.168.182.128); socklen_t addrlen sizeof(struct sockaddr_in); while (1) { // 3.捕获键盘输入 char buffer[1024] {0}; printf(客户端 say:); fflush(stdout); scanf(%s, buffer); // 4.发送数据 ssize_t ret sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)srvAddr, addrlen); CHECH_RESULT(ret, 发送数据失败); // 5.接收服务端响应进行展示 memset(buffer, 0x00, 1024); ret recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)srvAddr, addrlen); CHECH_RESULT(ret, 接收响应失败) printf(服务端 say:%s\n, buffer); } // 6.关闭套接字 close(sockfd); return 0; }五、TCP编程流程一编写流程TCP提供的是面向连接的、可靠的、字节流服务面向连接两端必须先建立连接才能通信可靠传输确保数据能够安全、有序的到达对端面向字节流以字节为单位进行传输不限制传输大小先发先到在TCP通信中套接字是区分了两种监听套接字是最早服务端通过socket创建的套接字只用于处理新连接通信套接字是客户端连接到服务器成功后创建的新的套接字用于与客户端通信sokcet()方法是用来创建一个套接字有了套接字就可以通过网络进行数据的收发创建套接字时要指定使用的服务类型使用TCP协议选择流式服务SOCK_STREAMbind()方法是用来指定套接字使用的IP地址和端口connect()方法一般由客户端程序执行用来与客户端建立连接listen方法时用来创建监听队列accept()用来从listen监听队列中接收一个连接返回一个新的连接socket该socket唯一地标识了被接收地这个连接send()方法用来向TCP连接的对端发送数据recv()方法用来接收TCP连接的对端发送来的数据close()关闭连接二代码编写1.服务端/* 实现一个简单的tcp服务端程序模拟简单聊天 1.接收客户端的消息进行展示 2.从键盘捕获输入发送给客户端 */ #include stdio.h #include string.h #include stdlib.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换htons #define CHECK_RESULT(ret, msg) \ if ((ret) 0) \ { \ perror(msg); \ return -1; \ } int main() { // 1.创建套接字 // int socket(int domain, int type, int protocol); int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(listen_fd, 创建套接字失败); // 2.构造监听地址结构对象为套接字绑定地址信息 // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9000); addr.sin_addr.s_addr inet_addr(192.168.182.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(listen_fd, (struct sockaddr *)addr, addrlen); CHECK_RESULT(ret, 绑定地址失败); // 3.开始监听 // int listen(int listen_fd, int backlog); ret listen(listen_fd, 1024); CHECK_RESULT(ret, 监听失败); while (1) { // 4.获取新连接与客户端的通信都是通过获取的新连接进行的 // int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen); // insten_fd:监听套接字描述符 // *addr:客户端地址信息 // *addrlen:指定想要获取的地址长度以及返回实际获取的地址长度 // 返回值:新建连接的套接字描述符用于往后的数据通信 struct sockaddr_in peerAddr; int newfd accept(listen_fd, (struct sockaddr *)peerAddr, addrlen); if (newfd 0) { perror(获取新连接出错); continue; } char peer_ip[24] {0}; inet_ntop(AF_INET, peerAddr.sin_addr, peer_ip, 23); printf(新的客户端连接建立成功[%s:%d]\n, peer_ip, ntohs(peerAddr.sin_port)); while (1) { // 5.接收客户端消息进行展示 // ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 返回值成功则返回世纪接收的数据长度 // 失败出错则返回-1 // 若返回0则表示连接断开 char buffer[1024] {0}; ret recv(newfd, buffer, 1023, 0); if (ret 0) { perror(接收数据出错); continue; } else if (ret 0) { printf(与客户端连接断开\n); close(newfd); break; } printf(客户端say:%s\n, buffer); // 6.捕获键盘输入向客户端发送数据 // ssize_t send(int sockfd, const void *buf, size_t len, int flags); memset(buffer, 0x00, 1024); printf(输入数据); fflush(stdout); scanf(%s, buffer); ret send(newfd, buffer, strlen(buffer), 0); if (ret ! strlen(buffer)) { if (ret 0) { perror(发送数据出错); close(newfd); continue; } printf(数据发送不完整); } } } // 7.关闭套接字 close(listen_fd); return 0; }2.客户端/* 实现一个简单的tcp服务端程序模拟简单聊天 1.从键盘捕获数据发送给服务器 2.等待服务器的响应进行展示 */ #include stdio.h #include string.h #include stdlib.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换htons #define CHECK_RESULT(ret, msg) \ if ((ret) 0) \ { \ perror(msg); \ return -1; \ } int main() { // 1.创建套接字 int sockfd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(sockfd, 创建套接字失败); // 2.绑定地址信息不主动 // 3.向服务器发送连接请求 // int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9000); addr.sin_addr.s_addr inet_addr(192.168.182.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret connect(sockfd, (struct sockaddr *)addr, addrlen); CHECK_RESULT(ret, 连接服务器失败); while (1) { // 4.捕获键盘数据向服务器发送数据 // ssize_t send(int sockfd, const void *buf, size_t len, int flags); char buffer[1024] {0}; memset(buffer, 0x00, 1024); printf(输入数据:); fflush(stdout); scanf(%s, buffer); ret send(sockfd, buffer, strlen(buffer), 0); if (ret 0) { perror(发送数据失败); close(sockfd); return -1; } // 5.等待服务端响应展示响应数据 // ssize_t recv(int sockfd, void *buf, size_t len, int flags); memset(buffer, 0x00, 1024); ret recv(sockfd, buffer, 1023, 0); if (ret 0) { perror(接收数据失败); close(sockfd); return -1; } printf(服务端 say:%s\n, buffer); } // 6.关闭套接字 close(sockfd); return 0; }六、多进程、多线程处理并发一并发原理tcp服务端存在的问题在socket套接字API中accept获取新连接recv接收数据send发送数据都是默认阻塞接口功能未完成就会一直等待这些阻塞接口导致当前在单执行流中无法正常的与每个客户端持续进行通信要么只能与一个客户端持续通信要么就只能与一个客户端通信一次解决方案将每一个阻塞操作都放到一个单独的执行流中多执行流有两种实现方案方案一多进程更加稳定健壮性更强方案二多线程成本更低线程间通信更加方便实现一个并发服务器可以同时与多个客户端进行通信的服务器服务端接受一个客户端的连接后创建一个线程或者进程然后在新创建的线程或进程中循环处理数据主线程父进程只负责监听客户端的连接并使用 accept() 接受连接不进行数据的处理。多执行流版本的并发服务器并不是一个好的解决方案资源有限需要为每一个客户端连接都创建一个执行流执行流并不是越多越好多了就会增加cou时间片切换成本二代码编写1.多进程服务端#include stdio.h #include string.h #include stdlib.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换htons #include signal.h #define CHECK_RESULT(ret, msg) \ if ((ret) 0) \ { \ perror(msg); \ return -1; \ } // 实现一个多进程版本的服务器创建进程在子进程中与客户端进行通信 void worker(int sockfd) { pid_t pid fork(); if (pid 0) { perror(创建子进程失败); return; } if (pid 0) { return; // 父进程直接返回回到main } while (1) { char buffer[1024] {0}; int ret recv(sockfd, buffer, 1023, 0); if (ret 0) { perror(接收数据出错); continue; } else if (ret 0) { printf(与客户端连接断开\n); break; } printf(客户端say: %s\n, buffer); // 6.捕获键盘输入向客户端发送数据 // ssize_t send(int sockfd, const void *buf, size_t len, int flags); memset(buffer, 0x00, 1024); printf(输入数据); fflush(stdout); scanf(%s, buffer); ret send(sockfd, buffer, strlen(buffer), 0); if (ret ! strlen(buffer)) { if (ret 0) { perror(发送数据出错); close(sockfd); continue; } printf(数据发送不完整\n); } } close(sockfd); exit(0); // 当前在子进程中这里退出的是子进程 } int main() { //主动忽略SIGCHLD信号告诉系统我不关注子进程状态 //显式的告诉系统子进程退出后直接释放所有资源 signal(SIGCHLD, SIG_IGN); // 1.创建套接字 // int socket(int domain, int type, int protocol); int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(listen_fd, 创建套接字失败); // 2.构造监听地址结构对象为套接字绑定地址信息 // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9000); addr.sin_addr.s_addr inet_addr(192.168.182.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(listen_fd, (struct sockaddr *)addr, addrlen); CHECK_RESULT(ret, 绑定地址失败); // 3. 开始监听 ret listen(listen_fd, 1024); CHECK_RESULT(ret, 监听失败); while (1) { struct sockaddr_in peerAddr; int newfd accept(listen_fd, (struct sockaddr *)peerAddr, addrlen); if (newfd 0) { perror(获取新链接出错); continue; } char peer_ip[24] {0}; inet_ntop(AF_INET, peerAddr.sin_addr, peer_ip, 23); printf(新的客户端连接建立成功: [%s:%d]\n, peer_ip, ntohs(peerAddr.sin_port)); worker(newfd); close(newfd); //父进程中并没有用到这个描述符 } return 0; }多进程注意事项通过信号处理避免产生僵尸进程父进程中创建子进程之后要关闭新建套接字父进程没用到2.多线程服务端#include stdio.h #include string.h #include stdlib.h #include signal.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换htons #include pthread.h #define CHECK_RESULT(ret, msg) \ if ((ret) 0) \ { \ perror(msg); \ return -1; \ } void *entry(void *arg) { int sockfd (int)arg; while (1) { // 5.接收客户端消息进行展示 // ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 返回值成功则返回世纪接收的数据长度 // 失败出错则返回-1 // 若返回0则表示连接断开 char buffer[1024] {0}; int ret recv(sockfd, buffer, 1023, 0); if (ret 0) { perror(接收数据出错); continue; } else if (ret 0) { printf(与客户端连接断开\n); break; } printf(客户端say:%s\n, buffer); // 6.捕获键盘输入向客户端发送数据 // ssize_t send(int sockfd, const void *buf, size_t len, int flags); memset(buffer, 0x00, 1024); printf(输入数据); fflush(stdout); scanf(%s, buffer); ret send(sockfd, buffer, strlen(buffer), 0); if (ret ! strlen(buffer)) { if (ret 0) { perror(发送数据出错); close(sockfd); continue; } printf(数据发送不完整); } } close(sockfd); } // 创建一个线程负责通过当前的描述符与客户端进行通信 void worker(int sockfd) { pthread_t tid; int ret pthread_create(tid, NULL, entry, (void *)sockfd); if (ret ! 0) { perror(创建工作线程失败); return; } pthread_detach(tid); // 分离线程让线程退出后自动释放所有资源不需要被等待 } int main() { // 主动忽略SIGCHLD信号——告诉系统我不关注子进程状态 // 显示的告诉系统子进程退出后直接释放所有资源 signal(SIGCHLD, SIG_IGN); // 1.创建套接字 // int socket(int domain, int type, int protocol); int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(listen_fd, 创建套接字失败); // 2.构造监听地址结构对象为套接字绑定地址信息 // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9000); addr.sin_addr.s_addr inet_addr(192.168.182.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(listen_fd, (struct sockaddr *)addr, addrlen); CHECK_RESULT(ret, 绑定地址失败); // 3.开始监听 // int listen(int listen_fd, int backlog); ret listen(listen_fd, 1024); CHECK_RESULT(ret, 监听失败); while (1) { // 4.获取新连接与客户端的通信都是通过获取的新连接进行的 // int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen); // insten_fd:监听套接字描述符 // *addr:客户端地址信息 // *addrlen:指定想要获取的地址长度以及返回实际获取的地址长度 // 返回值:新建连接的套接字描述符用于往后的数据通信 struct sockaddr_in peerAddr; int newfd accept(listen_fd, (struct sockaddr *)peerAddr, addrlen); if (newfd 0) { perror(获取新连接出错); continue; } char peer_ip[24] {0}; inet_ntop(AF_INET, peerAddr.sin_addr, peer_ip, 23); printf(新的客户端连接建立成功[%s:%d]\n, peer_ip, ntohs(peerAddr.sin_port)); worker(newfd); // close(newfd); // 线程间资源共享这里如果关闭描述符会导致普通线程无法使用 } // 7.关闭套接字 close(listen_fd); return 0; }多线程注意事项创建线程后要分离线程不需要被等待了主线程虽然不用newfd但是也不能关闭线程间共享进程资源