简单的Linux网络编程总结

news/2025/3/31 23:18:59/文章来源:https://www.cnblogs.com/Tohomson/p/18799134
在Linux环境下的基本网络编程步骤:
  1. 创建socket
  2. 绑定结构体
  3. 监听连接
  4. 建立连接
  5. 进行IO
  6. 关闭连接

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 是最通用的,但最老也最慢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907455.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式 + 分流器采样百安级大电流的微电压信号 + 微电压信号放大

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式## 分流器采样百安级大电流的微电压信号 OpAmp(运算放大器)微电压信号放大 如上图所示,\(\large V_{out} = V_{in} \times (1+ \dfrac{R_{2}}{R_{1}})\) TL431+MOS管,充满自停的充电器…

读DAMA数据管理知识体系指南34数据仓库和商务智能概念

读DAMA数据管理知识体系指南34数据仓库和商务智能概念1. 业务驱动因素 1.1. 主要驱动力是运营支持职能、合规需求和商务智能活动 1.2. 用数据来证明他们是合规的,因为数据仓库中包含历史数据,所以经常被用来响应这类要求 1.3. 商务智能支持一直是建设数据仓库的主要原因 2. 目…

环境检测 温湿度 噪声 建大仁科

环境检测 温湿度 噪声 建大仁科 1、温湿度 wifi版本 配置软件2、噪声 wifi 版本 配置软件 android手机上安装 蓝牙连接配置3、平台下载 RS-RJ-K监控平台-平台软件-温湿度传感器产品说明书下载及选型erwa.cn 二娃测试备忘

C# 13 中的新增功能实操

前言 今天大姚带领大家一起来看看 C# 13 中的新增几大功能,并了解其功能特性和实际应用场景。 前提准备 要体验 C# 13 新增的功能可以使用最新的 Visual Studio 2022 版本或 .NET 9 SDK 尝试这些功能。 Visual Studio 2022安装https://visualstudio.microsoft.com/zh-hans/dow…

Open R1 项目进展第一期

DeepSeek R1 发布已经两周了,而我们启动 open-r1 项目——试图补齐它缺失的训练流程和合成数据——也才过了一周。这篇文章简单聊聊:Open-R1 在模仿 DeepSeek-R1 流程和数据方面的进展 我们对 DeepSeek-R1 的认识和相关讨论 DeepSeek-R1 发布后社区搞出来的有趣项目这既是项目…

GPU内核实现(下)

3. ELLPACK 内核 ELLPACK SpMV实现沿行并行计算。由于数据已被重新排序为以列为主存储,因此沿ELLPACK数据连续行的内存访问被合并。在下面显示的实现中,假设输入cols和vals数组已经转换为ELLPACK格式。这种格式的一个关键部分是元数据参数,即每行非零的最大数量,它也作为参…

GPU内核实现(上)

GPU内核实现 以下是基于CSR和ELLPACK格式的一些标准SpMV实现。 1. 标量CSR内核 GPU加速SpMV的最简单实现之一是标量内核方法。标量内核分配一个线程来处理SpMV中的每个稀疏点积。稀疏点积由每个线程以顺序方式处理,从而消除了对需要共享内存和/或扭曲级别降低的更高级技术的需…

稀疏矩阵向量乘法介绍

稀疏矩阵向量乘法介绍 稀疏矩阵向量乘法(SpMV)是每个隐式稀疏线性代数求解器。从简单的 Krylov 算法到 multigrid 的算法性能方法在很大程度上取决于 SpMV 实现的速度。因为 SpMV 具有非常低的算术强度,定义为浮点操作数,则实现速度受内存带宽。最大化内存带宽的实现将实现…

推荐关注《AI芯片开发核心技术详解》(1)、《智能汽车传感器:原理设计应用》(2)、《TVM编译器原理与实践》(3)、《LLVM编译器原理与实践》(4)

4本书推荐《AI芯片开发核心技术详解》、《智能汽车传感器:原理设计应用》、《TVM编译器原理与实践》、《LLVM编译器原理与实践》由清华大学出版社资深编辑赵佳霓老师策划编辑的新书《AI芯片开发核心技术详解》已经出版,京东、淘宝天猫、当当等网上,相应陆陆续续可以购买。该…

Ollama+OneAPI+Open WebUI 搭建本地大模型

✅Ollama 安装 ✅极简安装 curl -fsSL https://ollama.com/install.sh | sh✅Docker 安装 ❗前提是已安装NVIDIA Container Toolkit # 拉取镜像 docker pull ollama/ollama# 启动容器 docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/o…

洛谷 P1216 [IOI 1994] 数字三角形 Number Triangles (记忆化搜索)

记忆化搜索思路:经典的DP题,看题解大佬个个是状态转移方程...我就写个记忆化搜索吧,这个数据量,只dfs暴搜是过不去的,写完记忆化之后发现有个测试点T了,下载了一波测试点数据,发现全是0,那么初始化dp数组为-1就好了。AcCode: #include<bits/stdc++.h> using nam…

【软件】在Windows和Ubuntu上使用TFTP和NFS

在Windows和Ubuntu上使用TFTP和NFS 零、介绍 最近在玩Linux开发板,在开发的过程中发现需要用到tftp和nfs来帮助传输文件,故此记录如何使用这两种软件。 TFTP(Trivial File Transfer Protocol) :是一种简化的文件传输协议,设计用于在客户端和服务器之间快速传输文件。轻量…