Linux学习第八天 —— socket服务器

张开发
2026/4/21 3:29:26 15 分钟阅读

分享文章

Linux学习第八天 —— socket服务器
目录一、环境配置1.在VM Ware上安装Ubuntu22.04虚拟机2.下载XShell 和 Xftp进行远程连接与文件传输3.在Windows上选择习惯的IDE进行编程习惯了使用CLion用CMake建立工程二、服务端代码三、select IO多路复用1.核心API2.fd_set大小为什么只能有这么大3.select底层原理4.select注意事项4.1错误14.2错误24.3错误34.4错误45.完整代码四、poll1.pollfd2.POLLIN、POLLOUT3.poll五、epoll1.int epoll_create(int size);2.struct epoll_event3.epoll_ctl4.epoll_wait5.底层原理6.水平触发与边缘触发6.1水平触发6.2边缘触发6.3阻塞、非阻塞IO7.eventpoll.c——epoll是不是线程安全的、是否支持mmap机制8.epoll示例代码8.1错误1、2、38.2错误4六、面向IO、面向事件1.面向IO2.面向事件七、Reactor模式八、Posix API1.unix api1.0TCP状态迁移图1.1客户端1.2服务端2.tcp control block3.listen(fd, backlog)中的backlog4.mtu 最大传输单元5.断开连接四次挥手6.双方同时调用connect建立完全平等的 tcp p2p连接6.1为什么公网 TCP P2P 很难6.2如果connect直接连接对方的公网IP呢6.3. 路由器为什么不让你进NAT 核心规则一、环境配置1.在VM Ware上安装Ubuntu22.04虚拟机2.下载XShell 和 Xftp进行远程连接与文件传输2.1用XShell连接Linux虚拟机。注意使用sudo ufw disable关闭防火墙不然能够ping通虚拟机但是无法连接。因为IMCP协议能够通过防火墙但是TCP协议被拦截。如果不关闭的话后续使用网络助手充当客户端也是连接不上虚拟机上运行的服务器的3.在Windows上选择习惯的IDE进行编程习惯了使用CLion用CMake建立工程3.1对CLion进行环境配置不然在开发的时候环境是Windows的环境不仅需要用宏定义区分平台Linux特有库的接口也无法进行代码补全。二、服务端代码// // Created by Administrator on 2026/4/13. // #include stdio.h #ifdef _WIN32 #define _WINSOCKAPI_ #include winsock2.h #include WS2tcpip.h #else #include sys/socket.h #include netinet/in.h #include unistd.h #include pthread.h #endif void *client_thread(void *arg) { int clientfd *(int *) arg; char buf[1024]; while(1) { int count recv(clientfd, buf, 1024, 0); printf(Received: %s\n, buf); count send(clientfd, Hello, Client!, sizeof (Hello, Client!), 0); printf(Send: %d\n,count); } return NULL; } int main(void) { printf(Hello, Server!\n); #ifdef _WIN32 // Windows 必须初始化 WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup 失败\n); return -1; } #endif int sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd 0) { printf(socket 创建失败\n); return -1; } struct sockaddr_in servAddr; servAddr.sin_family AF_INET; servAddr.sin_addr.s_addr htonl(INADDR_ANY); servAddr.sin_port htons(1234); int ret bind(sockfd, (struct sockaddr *) servAddr, sizeof(servAddr)); if (ret ! 0) { printf(bind 失败\n); return -1; } ret listen(sockfd, 10); if (ret ! 0) { printf(listen 失败\n); return -1; } struct sockaddr_in clientAddr; socklen_t clientAddrLen sizeof(clientAddr); while(1) { printf(accepting...\n); int clientfd accept(sockfd,(struct sockaddr *) clientAddr, clientAddrLen); printf(Connected\n); pthread_t th; pthread_create(th, NULL, client_thread, clientfd); } // char buf[1024]; // int count recv(clientfd, buf, 1024, 0); // printf(Received: %s\n,buf); // send(clientfd, Hello, Client!, sizeof (Hello, Client!), 0); printf(Server End...\n); #ifdef _WIN32 closesocket(sockfd); WSACleanup(); #else close(sockfd); #endif return 0; }此代码还有许多地方待优化例如线程的退出、连接中断后fd的关闭、循环的正常退出等。三、select IO多路复用1.核心APIfd_set fd容器最多1024size是写死的无法修改结构是bitmap按位FD_ZERO初始化fd_setFD_SET设置fd_setFD_ISSET判断fd是否有输入FD_CLR清空容器select五个参数int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);2.fd_set大小为什么只能有这么大初期设计如此0,1,2分别对应readfd、writefd、errfd3~1023都提供给用户使用认为够用。3.select底层原理轮询机制每调用一次select都会去校验1024个bit位是否有为14.select注意事项4.1错误1把 FD_ZERO FD_SET 写在了循环外面select 会把 fd_set 里 “没有事件” 的 fd 全部清空置 0下一次循环时老的客户端 fd 已经不在集合里了4.2错误2在 select 返回之后还在 FD_SET→ 这是完全错误的→FD_SET 必须在 select 调用之前4.3错误3刚 FD_SET 就判断 FD_ISSET→ 必然永远为 true→ 跟内核有没有数据无关4.4错误4没有在每次循环把所有客户端重新加入 fdSet→ 老客户端会失效void select_test(int sockfd) { int max_fd sockfd; struct sockaddr_in clientAddr; socklen_t clientAddrLen sizeof(clientAddr); fd_set fdSet; ///error1 { FD_ZERO(fdSet); // 错 FD_SET(sockfd,fdSet); // 错 } for (;;) { // 每次循环 没有重新清空、重新添加 fd int ret select(max_fd 1, fdSet, NULL, NULL, NULL); if(ret 0) { printf(Select Error!\n); break; } if(FD_ISSET(sockfd, fdSet)) { printf(accepting...\n); int clientfd accept(sockfd,(struct sockaddr *) clientAddr, clientAddrLen); printf(Connected\n); ///error2 { FD_SET(clientfd,fdSet); } if(clientfd max_fd) { max_fd clientfd; } } for (int fd sockfd 1; fd max_fd; fd) { ///error3 { FD_SET(fd,fdSet); // 错 } if(FD_ISSET(fd, fdSet)) { char buff[1024]; int count recv(fd,buff, 1024,0); if(count 0) { return; } if(count 0) { FD_CLR(fd,fdSet); close(fd); } printf(Recv Buff %s,buff); } } } }5.完整代码void select_test(int sockfd) { int max_fd sockfd; struct sockaddr_in clientAddr; socklen_t clientAddrLen sizeof(clientAddr); fd_set fdSet; for (;;) { FD_ZERO(fdSet); FD_SET(sockfd,fdSet); // 把所有客户端都加入监听必须在 select 之前加 for (int i sockfd 1; i max_fd; i) { FD_SET(i, fdSet); } int ret select(max_fd 1, fdSet, NULL, NULL, NULL); if(ret 0) { printf(Select Error!\n); break; } if(FD_ISSET(sockfd, fdSet)) { printf(accepting...\n); int clientfd accept(sockfd,(struct sockaddr *) clientAddr, clientAddrLen); if(clientfd max_fd) { max_fd clientfd; } } for (int fd sockfd 1; fd max_fd; fd) { if(FD_ISSET(fd, fdSet)) { char buff[1024]; int count recv(fd,buff, 1024,0); if(count 0) { return; } if(count 0) { FD_CLR(fd,fdSet); close(fd); } printf(Recv Buff %s,buff); } } } }四、poll1.pollfd2.POLLIN、POLLOUT3.poll五、epoll1.int epoll_create(int size);创建一个epoll的fdsize0就可以大小没有实际意义为了版本兼容。2.struct epoll_eventstruct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ } __EPOLL_PACKED;events对应可读可写宏定义data可以存储事件对应fd的值3.epoll_ctl/* Manipulate an epoll instance epfd. Returns 0 in case of success, -1 in case of error ( the errno variable will contain the specific error code ) The op parameter is one of the EPOLL_CTL_* constants defined above. The fd parameter is the target of the operation. The event parameter describes which events the caller is interested in and any associated user data. */ extern int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event) __THROW;可以控制把fd放入epollFd或者取消替换4.epoll_wait/* Wait for events on an epoll instance epfd. Returns the number of triggered events returned in events buffer. Or -1 in case of error with the errno variable set to the specific error code. The events parameter is a buffer that will contain triggered events. The maxevents is the maximum number of events to be returned ( usually size of events ). The timeout parameter specifies the maximum wait time in milliseconds (-1 infinite). This function is a cancellation point and therefore not marked with __THROW. */ extern int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);等待epoll中有事件触发返回值int表示有多少个fd被触发。可以从__events.events中获取事件触发的类型,有宏定义事件类型enum EPOLL_EVENTS { EPOLLIN 0x001, #define EPOLLIN EPOLLIN EPOLLPRI 0x002, #define EPOLLPRI EPOLLPRI EPOLLOUT 0x004, #define EPOLLOUT EPOLLOUT EPOLLRDNORM 0x040, #define EPOLLRDNORM EPOLLRDNORM EPOLLRDBAND 0x080, #define EPOLLRDBAND EPOLLRDBAND EPOLLWRNORM 0x100, #define EPOLLWRNORM EPOLLWRNORM EPOLLWRBAND 0x200, #define EPOLLWRBAND EPOLLWRBAND EPOLLMSG 0x400, #define EPOLLMSG EPOLLMSG EPOLLERR 0x008, #define EPOLLERR EPOLLERR EPOLLHUP 0x010, #define EPOLLHUP EPOLLHUP EPOLLRDHUP 0x2000, #define EPOLLRDHUP EPOLLRDHUP EPOLLEXCLUSIVE 1u 28, #define EPOLLEXCLUSIVE EPOLLEXCLUSIVE EPOLLWAKEUP 1u 29, #define EPOLLWAKEUP EPOLLWAKEUP EPOLLONESHOT 1u 30, #define EPOLLONESHOT EPOLLONESHOT EPOLLET 1u 31 #define EPOLLET EPOLLET };5.底层原理6.水平触发与边缘触发6.1水平触发只要有消息就一直触发适合消息大小固定的。6.2边缘触发有消息进来只触发一次需要循环去处理直到把所有消息都处理完成适合消息大小不固定的。6.3阻塞、非阻塞IO//设置非阻塞IO int set_nonblocking(int fd) { int flag fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK); return 0; }7.eventpoll.c——epoll是不是线程安全的、是否支持mmap机制8.epoll示例代码8.1错误1、2、3epoll_wait返回的int表示有多少个fd被触发events对应存储了多少个epoll_event被处罚的fd值其实是存储在epoll_event.data.fd里的并不是按顺序来的。8.2错误4边缘触发的时候需要使用set_nonblocking让listen、accept、recv变成非阻塞式的void epoll_demo(int socket,bool isLT) { if(!isLT) { set_nonblocking(socket); } int ep_fd epoll_create(10); struct epoll_event ev; ev.data.fd socket; if(isLT) { ev.events EPOLLIN; }else { ev.events EPOLLIN | EPOLLET; } int ret epoll_ctl(ep_fd, EPOLL_CTL_ADD, socket,ev); if(0 ! ret) { printf(epoll ctrl error : %d ,ret); return; } struct epoll_event events[MAX_FD]; for (;;) { int num epoll_wait(ep_fd, events, MAX_FD, -1); struct sockaddr_in clientAddr; socklen_t clientAddrLen sizeof(clientAddr); if(isLT) { for (int ii 0; ii num; ii) { int fd events[ii].data.fd; // if(ii socket) error1 if(events[ii].data.fd socket) { int clientFd accept(socket, (struct sockaddr *) clientAddr, clientAddrLen); ev.data.fd clientFd; int ret epoll_ctl(ep_fd,EPOLL_CTL_ADD,clientFd,ev); if(0 ! ret) { printf(epoll ctrl error); return; } }else { char buff[1024]; // int count recv(ii,buff, 1024, 0); error2 int count recv(fd,buff, 1024, 0); if(count 0) { printf(recv error \n); // close(ii); error3 close(fd); continue; } printf(Recv Buff %s\n,buff); } } }else { for (int ii 0; ii num; ii) { int fd events[ii].data.fd; if(fd socket) { while(1) { ///error4 没有set_nonblocking int clientFd accept(socket, (struct sockaddr *) clientAddr, clientAddrLen); if(clientFd 0) { break; } ev.data.fd clientFd; int ret epoll_ctl(ep_fd,EPOLL_CTL_ADD,clientFd,ev); if(0 ! ret) { printf(epoll ctrl error); return; } set_nonblocking(clientFd); } }else { while(1) { char buff[1024]; int count recv(fd,buff, 1024, 0); if(count 0) { printf(recv error \n); close(fd); break; }else { printf(Recv Buff %s\n,buff); if(errno EAGAIN || errno EWOULDBLOCK) { break; } } } } } } } }六、面向IO、面向事件1.面向IO代码层面明确IO事件发生后的动作。2.面向事件注册回调只处理事件不关心事件触发后的动作。listenfd -- EVENTIN -- accept_cbclientfd -- EVENTIN -- read_cbclietnfd -- EVENTOUT -- write_cb七、Reactor模式wrk测量qbs八、Posix API标准API类似于OpenGL对各个显卡厂商的统一接口规范。1.unix api1.0TCP状态迁移图seqnum —— 发出去的消息序号acknum —— 收到的消息序号1.1客户端socket();bind(); //optional , 可以不绑定connect(); //udpsend();recv();close();1.2服务端socket(); —— 插头fd插座tcp control block用bitmap表示fd是否可用bind(); —— 把 ip、port set到tcb对应的五元组src ip,src port,dst ip,dst port,protocol里listen(); —— tcb-status TCP_STATUS_LISTEN;—— tcb-syn_queue (半连接队列) ; tcb-accept_queue全连接队列在第一次握手时server端接收到连接请求会创建tcb其中会存储客户端的信息防止错误连接accept(); —— 在三次握手成功后进行accept1.分配fd2.fd tcbrecv();send();close()2.tcp control blocklisten在第一次接收到客户端请求创建tcb的时候tcp连接的生命周期就已经开始了。2.1syn_queue2.1.1syn泛洪2.2accept_queue3.listen(fd, backlog)中的backlogsyn队列 / synaccept队列总长未分配fd的tcb数量 / accept队列长度防止syn泛洪4.mtu 最大传输单元5.断开连接四次挥手5.1ack没收到先收到fin5.2双方同时调用close6.双方同时调用connect建立完全平等的 tcp p2p连接A调用connect连接BB调用connect连接AA、B在接收到SYN时发现自己也在连接对方于是进入TCP Simultaneous Open。6.1为什么公网 TCP P2P 很难不是协议不行是NAT 搞破坏A 发 SYN → BNAT 看到 “陌生外网 SYN” 直接丢包不回复 SYNACK握手断了 → 连不上所以公网 TCP 打洞看 NAT 脸色。但局域网里TCP P2P 100% 稳定。6.2如果connect直接连接对方的公网IP呢公网 IP 是路由器的不是你机器的你的机器只有内网 IP192.168.x.x所以数据包会发到对方路由器而不是对方电脑。6.3. 路由器为什么不让你进NAT 核心规则NAT 路由器有一条铁律凡是外部主动发进来的连接一律丢弃除非内网机器先向外发过包路由器才会建映射允许回来。否则你发 SYN → 对方路由器路由器查表没有这条记录直接丢包 或 回 RST 拒绝→ 你 connect超时 / 连接被重置

更多文章