目录
一、环境配置
1.在VM Ware上安装Ubuntu22.04虚拟机
2.下载XShell 和 Xftp进行远程连接与文件传输
3.在Windows上选择习惯的IDE进行编程(习惯了使用CLion,用CMake建立工程)
二、服务端代码
三、select IO多路复用
1.核心API
2.fd_set大小,为什么只能有这么大
3.select底层原理
4.select注意事项
4.1错误1
4.2错误2
4.3错误3
4.4错误4
5.完整代码
四、poll
1.pollfd
2.POLLIN、POLLOUT
3.poll
五、epoll
1.int epoll_create(int size);
2.struct epoll_event
3.epoll_ctl
4.epoll_wait
5.底层原理
6.水平触发与边缘触发
6.1水平触发
6.2边缘触发
6.3阻塞、非阻塞IO
7.eventpoll.c——epoll是不是线程安全的、是否支持mmap机制
8.epoll示例代码
8.1错误1、2、3
8.2错误4
六、面向IO、面向事件
1.面向IO
2.面向事件
七、Reactor模式
八、Posix API
1.unix api
1.0TCP状态迁移图
1.1客户端
1.2服务端
2.tcp control block
3.listen(fd, backlog)中的backlog
4.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.核心API
fd_set :fd容器,最多1024,size是写死的,无法修改,结构是bitmap,按位
FD_ZERO:初始化fd_set
FD_SET:设置fd_set
FD_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、errfd,3~1023都提供给用户使用,认为够用。
3.select底层原理
轮询机制,每调用一次select,都会去校验1024个bit位是否有为1,
4.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); } } } }四、poll
1.pollfd
2.POLLIN、POLLOUT
3.poll
五、epoll
1.int epoll_create(int size);
创建一个epoll的fd,size>0就可以,大小没有实际意义,为了版本兼容。
2.struct epoll_event
struct 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、3
epoll_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_cb
clientfd --> EVENTIN --> read_cb
clietnfd --> EVENTOUT --> write_cb
七、Reactor模式
wrk测量qbs
八、Posix API
标准API,类似于OpenGL对各个显卡厂商的统一接口规范。
1.unix api
1.0TCP状态迁移图
seqnum —— 发出去的消息序号
acknum —— 收到的消息序号
1.1客户端
socket();
bind(); //optional , 可以不绑定
connect(); //udp
send();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(); —— 在三次握手成功后进行accept
1.分配fd
2.fd <==> tcb
recv();
send();
close()
2.tcp control block
listen在第一次接收到客户端请求,创建tcb的时候,tcp连接的生命周期就已经开始了。
2.1syn_queue
2.1.1syn泛洪
2.2accept_queue
3.listen(fd, backlog)中的backlog
syn队列 / syn+accept队列总长,未分配fd的tcb数量 / accept队列长度(防止syn泛洪)
4.mtu 最大传输单元
5.断开连接(四次挥手)
5.1ack没收到,先收到fin
5.2双方同时调用close
6.双方同时调用connect,建立完全平等的 tcp p2p连接
A调用connect连接B;
B调用connect连接A;
A、B在接收到SYN时,发现自己也在连接对方,于是进入TCP Simultaneous Open。
6.1为什么公网 TCP P2P 很难?
不是协议不行,是NAT 搞破坏:
- A 发 SYN → B
- NAT 看到 “陌生外网 SYN” 直接丢包
- 不回复 SYN+ACK
- 握手断了 → 连不上
所以公网 TCP 打洞看 NAT 脸色。但局域网里,TCP P2P 100% 稳定。
6.2如果connect直接连接对方的公网IP呢?
- 公网 IP 是路由器的,不是你机器的
- 你的机器只有内网 IP:
192.168.x.x
所以数据包会发到对方路由器,而不是对方电脑。
6.3. 路由器为什么不让你进?(NAT 核心规则)
NAT 路由器有一条铁律:
凡是外部主动发进来的连接,一律丢弃!
除非:内网机器先向外发过包,路由器才会建映射,允许回来。
否则:
- 你发 SYN → 对方路由器
- 路由器查表:没有这条记录
- 直接丢包 或 回 RST 拒绝
→ 你 connect超时 / 连接被重置