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

    • 10backlog(等待连接队列的最大长度)

      • 表示最多允许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:设置要监听的事件类型

    • 可以是单个事件:EPOLLINEPOLLOUTEPOLLERR

    • 也可以是组合: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的监听事件

  • 常见场景:原本只监听读,现在需要同时监听写

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐