目录
3.在Windows上选择习惯的IDE进行编程(习惯了使用CLion,用CMake建立工程)
7.eventpoll.c——epoll是不是线程安全的、是否支持mmap机制
3.listen(fd, backlog)中的backlog
6.双方同时调用connect,建立完全平等的 tcp p2p连接
一、环境配置
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 超时 / 连接被重置

231

被折叠的 条评论
为什么被折叠?



