在Linux环境下的基本网络编程步骤:
- 创建socket
- 绑定结构体
- 监听连接
- 建立连接
- 进行IO
- 关闭连接
Linux实现网络编程的几个API
创建socket
//创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//函数原型
//int socket(int domain, int type, int protocol);
//domain:协议族,协议族(也叫地址族),如:AF_INET(IPv4),AF_INET6(IPv6),AF_UNIX(本地通信)
//type:套接字类型,如:SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
//通常设为 0,由系统自动匹配(TCP 是 IPPROTO_TCP,UDP 是 IPPROTO_UDP)
初始化网络结构体地址
//IPv4
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023, struct sockaddr_in {__uint8_t sin_len; //结构体长度sa_family_t sin_family; //协议族in_port_t sin_port; //端口struct in_addr sin_addr; //地址char sin_zero[8];//保留字段,用于填充
};struct in_addr {in_addr_t s_addr; //用于存储32位IP地址
};
//IPv6struct sockaddr_in6 servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin6_family = AF_INET6;
servaddr.sin6_port = htons(8080);
inet_pton(AF_INET6, "2001:db8::1", &servaddr.sin6_addr);
servaddr.sin6_flowinfo = 0;
servaddr.sin6_scope_id = 0; // 如果是 link-local 地址需要设置(如 wlan0 的 index)struct sockaddr_in6 {sa_family_t sin6_family; // 协议族(AF_INET6)in_port_t sin6_port; // 端口号(网络字节序)uint32_t sin6_flowinfo; // 流信息(QoS标识,可选),通常为0struct in6_addr sin6_addr; // IPv6 地址(128位)uint32_t sin6_scope_id; // 作用域 ID(用于本地链路地址),用于本地链路地址标识网卡
};struct in6_addr {unsigned char s6_addr[16]; // 16字节,即128位的IPv6地址
};
将网络结构体和socket进行绑定
bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr);
//函数原型
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:通过 socket() 创建的 socket 文件描述符
//addr:指向具体地址结构的指针(如 struct sockaddr_in*)
//addrlen:addr 所指结构体的大小(单位:字节)
//关于第二个参数:
//AF_INET:IPv4 - struct sockaddr_in
//AF_INET6:IPv6 - struct sockaddr_in6
//AF_UNIX:本地通信 - struct sockaddr_un 进程间通信
监听socket
listen(sockfd, 10);
//函数原型
//int listen(int sockfd, int backlog);
//sockfd:已通过 socket() 创建并 bind() 绑定的 socket
//backlog:等待队列的最大长度(连接排队上限)/* backlog 是什么? 这里backlog是全连接队列长度
*/
如果有连接到来,使用accept建立连接
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
//函数原型
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//从已进入监听状态(调用过 listen())的 socket 中 接受一个连接请求,并返回一个新的 socket,用于与客户端通信。
//sockfd:监听 socket,由 socket() + bind() + listen() 创建
//addr: 可选,传出参数,客户端的地址信息(如 IP 和端口)
//addrlen:传入一个长度变量,函数返回后会写入实际地址大小
建立好连接后就可以进行IO操作
send与recv
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//适用于:TCP socket(面向连接)
//flags 常用:0、MSG_DONTWAIT、MSG_NOSIGNAL
//返回值:成功传输的字节数;失败返回 -1
write()
/ read()
(通用 I/O)
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);//通用 I/O 接口,socket 也可以使用
//与 send()/recv() 本质一致,但缺少网络专用的 flags 参数
//不能传 MSG_DONTWAIT 等选项
sendto()
/ recvfrom()
(UDP / 无连接)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);//适用于 UDP / 原始 socket
//无需先调用 connect(),每次发送都指定目标地址
//可用于广播、组播、ICMP 等
writev()
/ readv()
(向 socket 批量发送多个 buffer)
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
struct iovec {void *iov_base; // 指向 buffersize_t iov_len; // 长度
};
//scatter/gather I/O,减少拷贝次数
//适合:多个连续 buffer(如 header + body)
//iovec 是一个数组,每个元素表示一个 buffer:
sendfile()
(文件→socket 零拷贝)
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
//in_fd 是文件描述符
//out_fd 是 socket fd
//内核态文件直接拷贝到 socket buffer,避免用户态数据拷贝
//常用于:静态文件下载、Web 服务、CDN
sendmsg()
/ recvmsg()
(高级结构发送)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);struct msghdr {void *msg_name; // 目的地址socklen_t msg_namelen;struct iovec *msg_iov; // 数据 buffer 数组int msg_iovlen;void *msg_control; // 控制数据(FD 传递)size_t msg_controllen;int msg_flags;
};//可以一次性发送多个 buffer + 控制消息(如传递文件描述符、附加信息)
//用于 UNIX 域 socket、内核级通信
close
int close(int fd);
//成功返回 0
//失败返回 -1,并设置 errno
//用户态 fd 被销毁
//内核中 socket 引用计数 -1
//若引用计数为 0,则释放 socket 结构及其关联缓冲区
//如果是 TCP socket,还会触发 TCP 四次挥手(主动断开连接)
一个简单的echo服务器代码
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>int main(){int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0servaddr.sin_port = htons(2000); // 0-1023, if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {printf("bind failed: %s\n", strerror(errno));}listen(sockfd, 10);printf("listen finshed: %d\n", sockfd); // 3 struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);printf("accept\n");int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed\n");char buffer[1024] = {0};int count = recv(clientfd, buffer, 1024, 0);printf("RECV: %s\n", buffer);count = send(clientfd, buffer, count, 0);printf("SEND: %d\n", count);return 0;
}
这只是一个最简单的echo服务器实现,编译运行后,可以有一个客户端进行连接,发送数据,数据发送后,服务器会将信息返回给客户端。
这个服务器每次只处理一条消息,就会关闭连接。
为了能持续和服务器进行数据交换,接下来使用while循环来持续处理来自客户端的消息,直到客户端断开连接
while (1) {printf("accept\n");int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed\n");char buffer[1024] = {0};int count = recv(clientfd, buffer, 1024, 0);printf("RECV: %s\n", buffer);count = send(clientfd, buffer, count, 0);printf("SEND: %d\n", count);}
这个版本的代码,也存在问题,那就是在while循环中处理收发之后,其他客户端建立连接可以成功,但是就无法处理其他连接的收发信息,因为整个服务器都在while循环中处理第一个连接收发。
接下来为了解决这个问题采用连接建立后新建线程的方式来处理建立好的连接:
void *client_thread(void *arg) {int clientfd = *(int *)arg;while (1) {char buffer[1024] = {0};int count = recv(clientfd, buffer, 1024, 0);if (count == 0) { // disconnectprintf("client disconnect: %d\n", clientfd);close(clientfd);break;}// parserprintf("RECV: %s\n", buffer);count = send(clientfd, buffer, count, 0);printf("SEND: %d\n", count);}}//main函数中的部分逻辑while (1) {printf("accept\n");int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed: %d\n", clientfd);pthread_t thid;pthread_create(&thid, NULL, client_thread, &clientfd);}
这样,就可以解决新的客户端连接进来,无法处理的问题。
这样每次新连接一个客户端,都会新建线程,线程是会消耗服务器资源的,所以这个办法,只能适用于在用户量不大的场景,如果用户量大,就会造成服务器资源耗尽而宕机。
接下来,使用Linux环境下的IO多路复用来解决这个问题
fd_set rfds, rset;FD_ZERO(&rfds);FD_SET(sockfd, &rfds);int maxfd = sockfd;while (1) {rset = rfds;int nready = select(maxfd+1, &rset, NULL, NULL, NULL);if (FD_ISSET(sockfd, &rset)) { // acceptint clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed: %d\n", clientfd);FD_SET(clientfd, &rfds); // if (clientfd > maxfd) maxfd = clientfd;}// recvint i = 0;for (i = sockfd+1; i <= maxfd;i ++) { // i fdif (FD_ISSET(i, &rset)) {char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0);if (count == 0) { // disconnectprintf("client disconnect: %d\n", i);close(i);FD_CLR(i, &rfds);continue;}printf("RECV: %s\n", buffer);count = send(i, buffer, count, 0);printf("SEND: %d\n", count);}} }
select
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);//nfds 所有 fd 中最大值 + 1(关键)
//readfds 你想监听“是否可读”的 fd 集合
//writefds 你想监听“是否可写”的 fd 集合
//exceptfds 异常 fd(如 OOB 数据)
//timeout 等待超时时间(可设置为 NULL 表示无限阻塞)FD_ZERO(&set); // 清空集合
FD_SET(fd, &set); // 添加 fd
FD_CLR(fd, &set); // 从集合移除 fd
FD_ISSET(fd, &set); // 检查 fd 是否被触发(可读 / 可写)//> 0 有 fd 可读/写/异常
//0 超时,无事件发生
//-1 出错(可能是信号中断)//select 会修改 fd_set 和 timeout,每次调用都要重置
//nfds 必须是最大 fd + 1
//最大支持监听 fd 数有限(通常是 1024),由 FD_SETSIZE 决定
//效率较低,适合小规模并发
select支持的fd是比较少的,而且虽然比使用多线程的方式效率高,但是性能同样偏低。
poll
poll()
与 select()
一样,是用于 I/O 多路复用 的系统调用,可以同时监视多个文件描述符(fd)是否可读、可写或出现异常。
它是 select()
的改进版本,支持更多 fd,使用数组而非位图,更灵活,避免了 FD_SETSIZE
限制。
struct pollfd fds[1024] = {0};fds[sockfd].fd = sockfd;fds[sockfd].events = POLLIN;int maxfd = sockfd;while (1) {int nready = poll(fds, maxfd+1, -1);if (fds[sockfd].revents & POLLIN) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed: %d\n", clientfd);//FD_SET(clientfd, &rfds); // fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;if (clientfd > maxfd) maxfd = clientfd;}int i = 0;for (i = sockfd+1; i <= maxfd;i ++) { // i fdif (fds[i].revents & POLLIN) {char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0);if (count == 0) { // disconnectprintf("client disconnect: %d\n", i);close(i);fds[i].fd = -1;fds[i].events = 0;continue;}printf("RECV: %s\n", buffer);count = send(i, buffer, count, 0);printf("SEND: %d\n", count);}}}
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds[] 要监听的 fd 数组,每个元素是一个 pollfd 结构
nfds fds[] 数组的大小
timeout 等待时间(单位:毫秒)
- 0: 立即返回
- -1: 无限等待
- >0: 最多等待多少毫秒struct pollfd {int fd; // 文件描述符short events; // 监听的事件short revents; // 实际发生的事件(由 poll 填写)
};POLLIN 可读(包括正常数据、关闭、错误)
POLLOUT 可写
POLLERR 错误
POLLHUP 对方挂断
POLLNVAL fd 非法
项目 | select() |
poll() |
---|---|---|
监听 fd 个数上限 | FD_SETSIZE 限制(默认1024) |
无上限(受系统资源限制) |
fd 管理方式 | 位图 fd_set |
数组 pollfd[] |
修改 fd 是否方便 | 不方便(需要重建 fd_set ) |
方便,修改数组即可 |
性能(fd 多时) | 每次遍历所有 fd | 也要遍历所有 fd(但不复制位图) |
跨平台性 | 较好 | 也很好(POSIX 标准) |
使用场景 | 建议 |
---|---|
想避免 FD_SETSIZE 限制 |
用 poll() 替代 select() |
高并发 / 大量连接 | 建议使用 epoll() (Linux)或 kqueue() (macOS) |
简单并发控制 | poll() 更清晰可维护 |
epoll的方案
epoll
(event poll)是 Linux 特有的 I/O 多路复用系统调用,旨在取代 select
/ poll
,提供更高性能、更大连接数支持的事件驱动机制。
int epfd = epoll_create(1);struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while (1) {struct epoll_event events[1024] = {0};int nready = epoll_wait(epfd, events, 1024, -1);int i = 0;for (i = 0;i < nready;i ++) {int connfd = events[i].data.fd;if (connfd == sockfd) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed: %d\n", clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else if (events[i].events & EPOLLIN) {char buffer[1024] = {0};int count = recv(connfd, buffer, 1024, 0);if (count == 0) { // disconnectprintf("client disconnect: %d\n", connfd);close(connfd);epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);continue;}printf("RECV: %s\n", buffer);count = send(connfd, buffer, count, 0);printf("SEND: %d\n", count);}}}
epoll_create() / epoll_create1() 创建一个 epoll 实例(得到一个 fd)
epoll_ctl() 向 epoll 实例中添加 / 修改 / 删除监听的 fd
epoll_wait() 等待就绪事件(阻塞或带超时)int epfd = epoll_create1(0); // 推荐使用 epoll_create1struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = sockfd; // 绑定的数据(fd)epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
添加监听项 EPOLL_CTL_ADD
修改监听项 EPOLL_CTL_MOD
删除监听项 EPOLL_CTL_DEL
//等待事件
struct epoll_event events[1024]; // 事件数组
int n = epoll_wait(epfd, events, 1024, -1); // 阻塞等待for (int i = 0; i < n; ++i) {if (events[i].events & EPOLLIN) {int ready_fd = events[i].data.fd;// 处理可读事件}
}
epoll事件
EPOLLIN 可读
EPOLLOUT 可写
EPOLLERR 错误
EPOLLHUP 对方关闭
EPOLLET 边缘触发(Edge Trigger)
EPOLLONESHOT 单次触发,触发后需手动重新添加
poll 模式对比
模式 | 含义 | 特点 |
---|---|---|
水平触发(LT) | 默认模式 | 每次都触发,只要 fd 满足条件 |
边缘触发(ET) | 高性能模式 | 状态变动时才触发,需非阻塞+读完所有数据 |
epoll 的优势
优势 | 描述 |
---|---|
不受 fd 数量限制 | 不再有 FD_SETSIZE 限制(百万级连接) |
内核事件通知机制 | epoll_wait 只返回就绪 fd,无需遍历全部 |
支持边缘触发(ET) | 事件发生变化时才通知,提高效率 |
零拷贝优化(结合 sendfile ) |
可降低内核→用户态开销 |
适合高并发、长连接、大量连接 | Web server / Proxy / 游戏后端常用 |
技术 | 模型 | 是否跨平台 | 性能 | 支持 fd 数量 |
---|---|---|---|---|
select |
轮询+数组 | ✅ 跨平台 | 较低 | 最多 1024(可改) |
poll |
轮询+链表 | ✅ 跨平台 | 中等 | 无固定上限 |
epoll |
事件驱动 | Linux 专用 | 高性能 | 极大(百万级) |
kqueue |
BSD 专用事件驱动 | BSD/macOS | 高性能 | 极大 |
IOCP |
Windows 专用 | ❌ | 高性能 | 极大 |
适用场景 | 建议 |
---|---|
小并发服务 | select 足够 |
中大型并发 | 推荐 poll() 或 epoll() (Linux) |
macOS 平台 | 推荐 kqueue |
想跨平台 | select 是最通用的,但最老也最慢 |