Linux网络编程(二)

这篇文章主要讲解常见的Linux网络编程模型select/poll/epoll对应接口的基础用法,侧重于服务端。

1. native模式

首先说一下最简单最朴素的模式。思路也很简单。这个是我自己起的名字,不是官方定的,面试的时候不要说native模式!
(1)创建套接字,这是一个唯一的文件描述符。函数:socket()
(2)套接字绑定服务IP和端口号。函数:bind();结构体:socketaddr_in
(3)设定端口等待连接数量上限。实际上控制的是全连接队列的长度,这个长度不能超过系统设定的队列长度最大值。函数:listen()。
(4)从监听套接字中接受客户端的连接。函数:accept().
(5)从已建立的连接中接收数据。函数:recv
(6)向客户端发送数据。函数:send()
代码:

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.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);
}

但是这么写存在一个问题,就是只能接受一个连接,发送完成数据后就自动关闭了,如果需要监听多个连接,需要使用多线程或者多进程技术改进。只需要把建立连接之后的代码替换就可以了。


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);
	}
}


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);

	}

2. select模式

native模式下,因为每次有一个连接进来,都要开一个线程或者进程进行读写事实上是非常消耗资源的。如果有成千上万的连接,线程会严重消耗cpu计算性能。因此要尽可能少的使用线程去管理连接。select就是一种在单线程模式下管理多个连接的方法。如何在单线程内管理多个连接呢?一种很自然的想法就是,不停的遍历文件描述符,有就绪的就进行操作,不就绪的就略过。这样不就可以避免多线程切换带来的开销吗。

int main() 
{
		// 创建一个IPv4的TCP套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	// 初始化服务器地址结构体
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;          // 协议族为IPv4
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意IP地址
	servaddr.sin_port = htons(2000);      // 监听端口为2000
	
	// 将套接字与IP地址和端口绑定
	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
	    printf("bind failed: %s\n", strerror(errno));
	}
	
	// 开始监听,最多允许10个连接排队等待
	listen(sockfd, 10);
	printf("listen finished: %d\n", sockfd);
	
	// 用于存储客户端地址信息
	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);
	
	// 文件描述符集合,用于select函数监控
	fd_set rfds, rset;
	FD_ZERO(&rfds); // 初始化rfds为空集
	FD_SET(sockfd, &rfds); // 将服务器监听套接字加入集合
	int maxfd = sockfd; // 当前监控的最大文件描述符
	
	// 主循环处理连接和通信
	while (1) {
	    // 每次循环前复制rfds到rset,避免select直接修改原始集合
	    rset = rfds;
	    
	    // 调用select等待文件描述符变为可读或其他状态,返回就绪的文件描述符数量
	    int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
	    
	    // 检查是否有新的连接请求
	    if (FD_ISSET(sockfd, &rset)) { 
	        // 接受新的连接
	        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
	        printf("accept finished: %d\n", clientfd);
	        
	        // 将新客户端的套接字加入监控集合
	        FD_SET(clientfd, &rfds);  
	        
	        // 更新监控的最大文件描述符
	        if (clientfd > maxfd) {
	            maxfd = clientfd;
	        }
	    }
	
	    // 遍历所有可能的客户端连接,新建立的连接下次遍历才能收发数据
	    for (int i = sockfd+1; i <= maxfd; i++) {
	        // 检查该文件描述符是否可读(有数据可接收)
	        if (FD_ISSET(i, &rset)) {
	            char buffer[1024] = {0};
	            
	            // 从客户端接收数据
	            int count = recv(i, buffer, 1024, 0);
	            
	            // 如果recv返回0,表示客户端关闭连接
	            if (count == 0) {
	                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);
	        }
	    }
	}
}

3. poll模式

poll模式实际上是对select模式的改进。select函数在大多数Unix和类Unix系统中受限于一个硬编码的常量,这个常量定义了单个进程可以监视的文件描述符的最大数量。这个限制通常被称为FD_SETSIZE,它通常默认设置为一个相对有限的数量,比如1024或2048。poll使用一个结构体数组进行动态监视。

int main() {
    // 创建一个IPv4的TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 初始化服务器地址结构体
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;          // 协议族为IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到本机的所有IP地址
    servaddr.sin_port = htons(2000);      // 服务器监听端口设为2000

    // 将套接字与IP地址和端口号绑定
    if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
        printf("bind failed: %s\n", strerror(errno));
    }

    // 开始监听,最多允许有10个连接请求排队等待处理
    listen(sockfd, 10);
    printf("listen finished: %d\n", sockfd); // 输出监听的套接字描述符

    // 用于存储客户端地址信息
    struct sockaddr_in  clientaddr;
    socklen_t len = sizeof(clientaddr);

    // 初始化pollfd数组用于poll函数,大小为1024,用于监控多个文件描述符
    struct pollfd fds[1024] = {0};
    // 将监听套接字添加到pollfd数组中,监控其可读事件
    fds[sockfd].fd = sockfd;
    fds[sockfd].events = POLLIN;

    int maxfd = sockfd; // 当前最大的文件描述符,初始为监听套接字

    // 主循环处理连接和通信
    while (1) {
        // 调用poll函数等待文件描述符变为可读或其他状态,-1表示无限等待
        int nready = poll(fds, maxfd+1, -1);

        // 检查监听套接字是否有新的连接请求
        if (fds[sockfd].revents & POLLIN) {

            // 接受新的连接请求
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("accept finished: %d\n", clientfd);

            // 将新连接的文件描述符添加到pollfd数组中,并设置监控其可读事件
            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;
            
            // 更新最大文件描述符
            if (clientfd > maxfd) maxfd = clientfd;

        }
        
        // 遍历所有可能的客户端连接
        for (int i = sockfd+1; i <= maxfd;i ++) {
            // 检查当前文件描述符是否有数据可读
            if (fds[i].revents & POLLIN) {

                char buffer[1024] = {0};
                
                // 从客户端接收数据
                int count = recv(i, buffer, 1024, 0);
                
                // 如果recv返回0,表示客户端关闭连接
                if (count == 0) {
                    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);
            }
        }
    }
}

4. epoll模式

无论是native、select还是poll,操作的都需要对文件描述符进行遍历。如果连接比较少,那么是没有问题的,如果连接非常多,每个连接需要过很长时间才能被扫描一次,这个时候用户体验会非常糟糕,服务会出现卡顿、无响应的情况,在硬实时任务中尤为致命。
有没有办法能够高效遍历连接呢?或者说能不能只对就绪的连接进行操作?一个非常自然的想法就是,当一个连接准备就绪的时候,直接把自己放到一个集合中,程序只需要遍历这个集合就可以了。epoll正是基于这种思想实现。

int main() {
    // 创建一个IPv4的TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 初始化服务器地址结构体
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;          // 地址族为IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意IP地址
    servaddr.sin_port = htons(2000);      // 设置监听端口为2000

    // 尝试将套接字与指定地址和端口绑定
    if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
        printf("bind failed: %s\n", strerror(errno));
    }

    // 开始监听,第二个参数为监听队列的最大长度
    listen(sockfd, 10);
    printf("listen finished: %d\n", sockfd); // 打印监听套接字的文件描述符

    // 准备用于存储客户端地址信息的结构体
    struct sockaddr_in  clientaddr;
    socklen_t len = sizeof(clientaddr);

    // 创建一个epoll实例
    int epfd = epoll_create(1);

    // 初始化一个epoll_event结构体,用于添加监听套接字到epoll实例
    struct epoll_event ev;
    ev.events = EPOLLIN;       // 设置事件类型为可读
    ev.data.fd = sockfd;       // 设置关联的文件描述符为监听套接字
    // 将监听套接字添加到epoll实例中
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

    // 主循环处理连接和通信
    while (1) {
        // 准备epoll_event数组用于存储就绪事件,最大1024个
        struct epoll_event events[1024] = {0};
        // 调用epoll_wait等待事件发生,-1表示无限期等待
        int nready = epoll_wait(epfd, events, 1024, -1);

        // 遍历所有就绪的事件
        for (int i = 0; i < nready; i++) {
            int connfd = events[i].data.fd; // 获取触发事件的文件描述符

            // 检查是否为监听套接字事件
            if (connfd == sockfd) {
                // 接受新的连接请求
                int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
                printf("accept finished: %d\n", clientfd);

                // 初始化一个新的epoll_event,用于添加新连接到epoll
                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                // 将新连接的文件描述符添加到epoll实例中
                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) { // 如果接收数据为0,表示客户端关闭连接
                    printf("client disconnect: %d\n", connfd);
                    close(connfd);
                    // 从epoll实例中删除已关闭的连接
                    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);
            }
        }
    }
}

这是一条吃饭博客,由挨踢零声赞助

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值