简单的Linux网络编程总结

在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地址
};

//IPv6

struct 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标识,可选),通常为0
    struct 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; // 指向 buffer
    size_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.0
	servaddr.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) { // disconnect
			printf("client disconnect: %d\n", clientfd);
			close(clientfd);
			break;
		}
		// parser
		
		printf("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)) { // accept
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept finshed: %d\n", clientfd);
			FD_SET(clientfd, &rfds); // 
			if (clientfd > maxfd) maxfd = clientfd;
		}
		// recv
		int i = 0;
		for (i = sockfd+1; i <= maxfd;i ++) { // i fd
			if (FD_ISSET(i, &rset)) {
				char buffer[1024] = {0};
				int count = recv(i, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("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 fd

			if (fds[i].revents & POLLIN) {

				char buffer[1024] = {0};
				
				int count = recv(i, buffer, 1024, 0);
				if (count == 0) { // disconnect
					printf("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) { // disconnect
					printf("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_create1

struct 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 是最通用的,但最老也最慢
posted @ 2025-03-29 08:59  Tohomson  阅读(67)  评论(0)    收藏  举报