tcp细节
1. 创建Socket
cpp
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
功能:创建一个网络通信端点(socket)
-
参数说明:
-
AF_INET:使用IPv4协议族(Address Family) -
SOCK_STREAM:使用TCP协议(面向连接的流式套接字) -
0:协议自动选择(TCP对应IPPROTO_TCP)
-
-
返回值:
sockfd是socket文件描述符(类似文件句柄),后续操作都基于它 -
类比:就像打电话前先要有一部电话机
2. 定义服务器地址结构
cpp
struct sockaddr_in serveraddr;
-
功能:定义一个IPv4地址结构体变量
-
包含信息:IP地址、端口号、协议族
-
类比:相当于填写收信人的地址信息
3. 清空地址结构
cpp
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
-
功能:将serveraddr结构体所有字节设置为0
-
参数:
-
&serveraddr:要清空的地址 -
0:填充的值 -
sizeof(...):清空的大小
-
-
作用:确保结构体中无垃圾数据,只使用明确设置的字段
4. 设置协议族
cpp
serveraddr.sin_family = AF_INET;
-
功能:指定地址族为IPv4
-
必须与socket()中保持一致
5. 设置IP地址
cpp
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
INADDR_ANY:通配地址(0.0.0.0)-
表示监听服务器上所有网络接口(包括localhost、局域网IP等)
-
-
htonl():Host TO Network Long-
将主机字节序转换为网络字节序(大端序)
-
-
s_addr:32位IP地址 -
好处:服务器有多个IP时,自动接受发往任一IP的连接
6. 设置端口号
cpp
serveraddr.sin_port = htons(2048);
-
2048:自定义端口号(1024以下需要root权限) -
htons():Host TO Network Short-
将16位端口号转换为网络字节序
-
-
类比:相当于确定打电话时的分机号
7. 绑定地址到socket
cpp
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
-
功能:将socket与指定的IP和端口绑定
-
参数:
-
sockfd:之前创建的socket -
(struct sockaddr*):强制类型转换(通用socket地址类型)切换成addr型,从而能够识别出来。直接用addr不好用 -
struct sockaddr_in(IPv4)的内存布局:
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ sin_family │ sin_port │ sin_addr │ sin_zero │
│ (2 bytes) │ (2 bytes) │ (4 bytes) │ (8 bytes) │
└─────────────┴─────────────┴─────────────┴─────────────┘
总大小: 16字节强制转换为 struct sockaddr 后,内核这样看:
┌─────────────┬─────────────────────────────────┐
│ sa_family │ sa_data │
│ (2 bytes) │ (14 bytes) │
└─────────────┴─────────────────────────────────┘
总大小: 16字节 -
sizeof(...):地址结构体大小
-
-
检查:bind失败返回-1,打印错误信息
-
类比:相当于给电话机分配电话号码
-
常见错误:端口已被占用(Address already in use)
8. 监听连接
cpp
listen(sockfd, 10);
-
功能:将socket变为被动监听状态,等待客户端连接
-
参数:
-
sockfd:绑定了地址的socket -
10:backlog(等待连接队列的最大长度)-
表示最多允许10个客户端等待连接
-
超过的客户端会收到连接拒绝
-
-
-
类比:电话铃响时,最多有10个人同时等待接听
连接列表初始化部分
c
connlist[sockfd].fd = sockfd;
-
将当前socket文件描述符存储到连接列表中
-
connlist应该是一个全局的连接信息数组 -
以sockfd作为索引,便于快速查找
c
connlist[sockfd].recv_t.accept_callback = accept_cb;
-
设置接收回调函数
-
accept_cb应该是处理新连接的回调函数 -
这体现了事件驱动的编程思想
epoll创建部分
c
epfd = epoll_create(1); //int size
-
创建epoll实例
-
参数1是提示内核epoll事件表的大小(Linux 2.6.8后该参数被忽略,但必须>0)
-
返回epoll文件描述符,用于后续的epoll操作
事件注册部分
c
set_event(sockfd, EPOLLIN, 1);
-
自定义函数,将sockfd添加到epoll监听
-
监听EPOLLIN(可读)事件
-
第三个参数1可能表示添加事件(不是修改或删除)
事件数组定义
c
struct epoll_event events[1024] = {0};
-
定义事件数组,用于存储epoll_wait返回的事件
-
大小1024,表示一次最多处理1024个事件
-
{0}将数组所有元素初始化为0
主循环开始
c
while(1){
-
无限循环,持续处理网络事件
epoll等待事件
c
int nready = epoll_wait(epfd, events, 1024, -1);
-
等待事件发生
-
参数:epfd(epoll实例),events(返回的事件数组),1024(最大事件数),-1(无限超时)
-
返回就绪的事件数量
事件处理循环
c
int i = 0; for(i=0; i < nready; i++)
-
遍历所有就绪的事件
获取连接描述符
c
int connfd = events[i].data.fd;
-
从事件数据中获取触发事件的socket描述符
-
data.fd通常存储socket描述符
可读事件处理
c
if (events[i].events & EPOLLIN){
-
检查事件类型是否为可读事件
-
使用位与操作判断
c
int count = connlist[connfd].recv_t.recv_callback(connfd);
-
调用对应的接收回调函数
-
从连接列表中获取该连接的回调函数
-
传入connfd参数,回调函数负责实际的数据接收
c
printf("recv count: %d<-- buffer:%s\n", count, connlist[connfd].rbuffer);
-
打印接收到的数据信息
-
count:接收的字节数
-
rbuffer:存储接收数据的缓冲区
可写事件处理
c
} else if(events[i].events & EPOLLOUT){
-
处理可写事件(当socket发送缓冲区可写时触发)
c
printf("send --> buffer: %s\n", connlist[connfd].wbuffer);
int count = connlist[connfd].send_callback(connfd);
-
打印要发送的数据
-
调用发送回调函数进行实际的数据发送
-
wbuffer:待发送的数据缓冲区
主循环结束
c
} }
-
结束事件处理循环
-
结束while无限循环
程序暂停
c
getchar();
-
等待用户输入字符
-
防止程序立即退出
-
通常用于调试或保持服务器运行
结构体 conn_item 详细讲解
这是一个网络连接项的结构体定义,用于管理每个socket连接的相关数据和回调函数。
1. 基础成员
c
int fd;
-
文件描述符:存储socket连接的文件描述符
-
作用:唯一标识一个网络连接
-
范围:通常是大于等于3的整数(0、1、2分别代表stdin、stdout、stderr)
2. 接收缓冲区相关
c
char rbuffer[BUFFER_LENGTH]; int rlen;
-
rbuffer:接收数据缓冲区
-
存储从socket接收到的数据
-
BUFFER_LENGTH应该是预定义的宏,如#define BUFFER_LENGTH 1024 -
循环利用,用于暂存接收的数据
-
-
rlen:接收数据长度
-
记录当前rbuffer中有效数据的长度
-
范围:0 到 BUFFER_LENGTH
-
用于跟踪已接收但未处理的数据量
-
3. 发送缓冲区相关
c
char wbuffer[BUFFER_LENGTH]; int wlen;
-
wbuffer:发送数据缓冲区
-
存储待发送到socket的数据
-
同样大小由BUFFER_LENGTH定义
-
用于暂存要发送的数据
-
-
wlen:待发送数据长度
-
记录wbuffer中待发送的数据长度
-
用于发送时知道要发送多少字节
-
发送完成后通常重置为0
-
4. 回调函数联合体
c
union{
RCALLBACK accept_callback;
RCALLBACK recv_callback;
} recv_t;
这是一个联合体(union),它有两个成员但共享同一块内存:
-
设计意图:
-
同一个连接不可能同时既是监听socket又是普通连接socket
-
监听socket需要accept_callback(接受新连接)
-
普通连接需要recv_callback(接收数据)
-
使用union可以节省内存
-
-
accept_callback:接受连接回调
-
仅用于监听socket
-
当有新连接到达时调用
-
负责accept新连接并初始化
-
-
recv_callback:接收数据回调
-
仅用于已建立的连接
-
当有数据可读时调用
-
负责从socket读取数据到rbuffer
-
5. 发送回调
c
RCALLBACK send_callback;
-
send_callback:发送数据回调
-
用于所有需要发送数据的连接
-
当socket可写(EPOLLOUT事件)时调用
-
负责从wbuffer发送数据到socket
-
6. 回调函数类型
c
typedef int (*RCALLBACK)(int fd);
假设RCALLBACK是这样定义的:
-
是一个函数指针类型
-
参数:int fd(连接的文件描述符)
-
返回值:int(通常表示处理的数据长度或状态)
函数 set_event 详细讲解
这是一个用于操作epoll事件的自定义函数,主要功能是向epoll实例添加新事件或修改已存在事件。
函数签名
c
int set_event(int fd, int event, int flag)
-
返回值:int(但函数中没有return语句,这是不规范的)
-
参数1 fd:要监控的文件描述符
-
参数2 event:要监控的事件类型(如EPOLLIN、EPOLLOUT)
-
参数3 flag:操作标志(1表示添加,0表示修改)
函数内部逻辑
1. 参数判断
c
if(flag){ //1 add, 0 mod
-
flag作为布尔值使用 -
非0(通常是1):执行添加操作
-
0:执行修改操作
2. 添加事件分支(flag为真)
c
struct epoll_event ev; ev.events = event; ev.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
详细说明:
-
struct epoll_event ev:声明epoll事件结构体 -
ev.events = event:设置要监听的事件类型-
可以是单个事件:
EPOLLIN、EPOLLOUT、EPOLLERR等 -
也可以是组合:
EPOLLIN | EPOLLET(边缘触发)
-
-
ev.data.fd = fd:将文件描述符存入data联合体-
当事件触发时,epoll_wait会返回这个data
-
也可以存储指针:
ev.data.ptr = &some_struct
-
-
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev):-
epfd:全局的epoll文件描述符 -
EPOLL_CTL_ADD:操作类型,表示添加 -
将fd和对应的事件ev添加到epoll监听列表
-
3. 修改事件分支(flag为假)
c
struct epoll_event ev; ev.events = event; ev.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
-
基本结构与添加分支相同
-
区别在于使用
EPOLL_CTL_MOD操作类型 -
用于修改已存在的fd的监听事件
-
常见场景:原本只监听读,现在需要同时监听写
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)