突破网络瓶颈:提升性能的必备技术——Linux网络IO与select详解

举报
Lion Long 发表于 2023/07/15 13:38:57 2023/07/15
【摘要】 本文通过对Linux网络IO和select的详细讨论,帮助读者深入理解了这些关键概念,并展示了select函数在构建高效网络应用中的重要性和灵活性。对于想要提升网络编程技能的开发者来说,这些知识将会是宝贵的参考和实践指南。

一、IO的定义

IO 即“Input”和“Output”的组合,即输入/输出,IO用来处理设备之间的数据传输。socket/fd也是一种IO。

二、socket的定义

socket 的译意是“插座”,在计算机通信领域,socket 也被翻译为“套接字”,它是计算机之间进行通信的一种方式。通过 socket ,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

三、一对一服务器设计

image.png

第一步:创建socket。
函数原型

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

这个函数建立一个协议族、协议类型、协议编号的socket文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
domain参数值含义:

名称 含义
PF_UNIX,PF_LOCAL 本地通信
AF_INET,PF_INET IPv4协议
PF_INET6 IPv6协议
PF_NETLINK 内核用户界面设备
PF_PACKET 底层包访问

type参数值含义:

名称 含义
SOCK_STREAM TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM UDP连接
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
:SOCK_PACKET 专用类型
SOCK_RDM 提供可靠的数据报文,不保证数据有序
SOCK_RAW 提供原始网络协议访问

protocol参数含义:
通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;如果协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
第二步:设置参数
通过struct sockaddr_in结构体指定协议族,指定绑定地址,指定监控的端口号。
使用的成员:sin_family、sin_addr.s_addr、sin_port
第三步:绑定–> bind
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第2个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
第3个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败
第四步:监听–> listen
函数原型:

#include<sys/socket.h>
int listen(int sockfd, int backlog);

参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第二个参数规定了内核应该为相应套接字排队的最大连接个数。
第五步:接收连接–> accept
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

参数说明:
sockefd:套接字描述符,该套接字在listen()后监听连接。
addr:(可选)指针。指向一个缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针。输入参数,配合addr一起使用,指向存有addr地址长度的整形数。
第六步:接收数据–> recv
函数原型:

#include<sys/types.h>
#include<sys/socket.h>
int recv( int fd, char *buf, int len, int flags);

参数说明:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
第七步:发送数据–>send
函数原型:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:
sockfd:向套接字中发送数据
buf:要发送的数据的首地址
len:要发送的数据的字节
int flags:设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样
返回值:成功返回实际发送的字节数,失败返回 -1

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#define BUFF_LENGTH 128
int main(void)
{
	int listen_fd=socket(AF_INET,SOCK_STRAM,0);
	if(lisenfd==-1)
		return -1;
	printf("lisenfd: %d\n",lisenfd);
	
	struct sockaddr_in servaddr;
	servaddr.sin_family=AF_INET;//指定协议族,INET是IPv4,INET6是IPv6
	servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//指定地址
	servaddr.sin_port=htons(9999);//将整型变量从主机字节顺序转变成网络字节顺序
	
	//bind(listenfd,&servaddr,sizeof(servaddr));
	if(-1==bind(lisenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)))
	{
		return -2;
	}
	
	listen(listenfd,10);

	struct sockaddr_in client;
	socklen_t len=sizeof(client);
	int clientfd=accept(lisenfd,(struct sockaddr*)&client,&len);
	printf("client: %d\n",clientfd);

	while(1){
		unsigned char buffer[BUFF_LENGTH] = { 0 };
		int ret = recv(clientfd,buffer,BUFF_LENGTH,0);
		printf("buffer: %s,ret=%d\n",buffer,ret);
		
		ret=send(clientfd,buffer,ret,0);
		printf("send buffer: %s,ret=%d\n",buffer,ret);
	}
	return 0;
}

四、设置非阻塞

默认的连接是阻塞方式的,可以使用fcntl函数进行设置非阻塞模式。
函数原型:

#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
// 返回值:成功依赖cmd的值,失败返回-1;

cmd参数说明:

参数 含义
F_GETFL 获取文件状态标志
F_SETFL 设置文件状态标志
F_GETFD 获取文件描述符标志
F_SETFD 设置文件描述符标志
F_GETLK 获取文件锁
F_SETLK 设置文件锁
F_DUPFD 复制文件描述符
F_GETOWN 取当前接受SIGIO和SIGURG信号的进程ID和进程组ID.正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程中ID
F_SETOWN 设置当前接受SIGIO和SIGURG信号的进程ID和进程组ID.

状态标志:

标志 含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 为读、写打开
O_APPEND 每次写时追加
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(数据)
O_RSYNC 同步读、写
O_FSYNC 等待写完成(进FreeBSD和Mac OS X)
O_ASYNC 异步I/O(进FreeBSD和Mac OS X)

注意: 非阻塞要在accpt函数之前设置才能生效。
使用示例:

int flag=fcntl(listenfd,F_GETFL,0);
flg|=O_NONBLOCK
fcntl(listenfd,F_SETFL,0);

五、多对一服务器设计

5.1、多线程方案

使用多线程方案,来一个连接请求则创建一个线程。
image.png

pthread_create函数原型:

#include <pthread.h>
int pthread_create(
                 pthread_t *restrict tidp,   				//新创建的线程ID指向的内存单元。
                 const pthread_attr_t *restrict attr,		//线程属性
                 void *(*start_rtn)(void *), 				//线程函数的地址
                 void *restrict arg 						//线程函数所需的参数
                  );

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

#define BUFFER_LENGTH	128

// thread --> fd
void *routine(void *arg) 
{
	int clientfd = *(int *)arg;
	while (1) {
		unsigned char buffer[BUFFER_LENGTH] = {0};
		int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		if (ret == 0) 
		{
			close(clientfd);
			break;
		}
		printf("buffer : %s, ret: %d\n", buffer, ret);
		ret = send(clientfd, buffer, ret, 0); 
	}
}

int main() {
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
	if (listenfd == -1) return -1;

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
		return -2;
	}

#if 0 // nonblock
	int flag = fcntl(listenfd, F_GETFL, 0);
	flag |= O_NONBLOCK;
	fcntl(listenfd, F_SETFL, flag);
#endif

	listen(listenfd, 10);
	
	while (1) {
		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
		
		pthread_t threadid;
		pthread_create(&threadid, NULL, routine, &clientfd);
	}
	return 0;
}

5.2、io多路复用——select

image.png

什么是IO多路复用? 通俗的讲就是一个线程,通过记录IO流的状态来管理多个IO。解决创建多个进程处理IO流导致CPU占用率高的问题。
select是io多路复用的一种方式,其他的还有poll、epoll等。
函数原型:

#include <sys/types.h>
#include <unistd.h>

int select(int maxfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select函数共有5个参数,其中参数和返回值:
maxfds:监视对象文件描述符数量。
readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
timeout:调用select后,为防止陷入无限阻塞状态,传递超时信息。
返回值:错误返回-1,超时返回0。当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

完整示例:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_LENGTH	128

int main() {

	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
	if (listenfd == -1) return -1;
// listenfd
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
		return -2;
	}

#if 0 // nonblock
	int flag = fcntl(listenfd, F_GETFL, 0);
	flag |= O_NONBLOCK;
	fcntl(listenfd, F_SETFL, flag);
#endif

	listen(listenfd, 10);

	fd_set rfds, wfds, rset, wset;
	FD_ZERO(&rfds);
	FD_SET(listenfd, &rfds);
	FD_ZERO(&wfds);

	int maxfd = listenfd;
	
	unsigned char buffer[BUFFER_LENGTH] = {0}; // 0 
	int ret = 0;
	// int fd, 
	while (1) {
		rset = rfds;
		wset = wfds;

		int nready = select(maxfd+1, &rset, &wset, NULL, NULL);
		if (FD_ISSET(listenfd, &rset)) {

			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
			
			FD_SET(clientfd, &rfds);

			if (clientfd > maxfd) maxfd = clientfd;
		} 
		
		int i = 0;
		for (i = listenfd+1; i <= maxfd;i ++) {

			if (FD_ISSET(i, &rset)) { //

				ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret == 0) {
					close(i);
					FD_CLR(i, &rfds);
					
				} else if (ret > 0) {
					printf("buffer : %s, ret: %d\n", buffer, ret);
					FD_SET(i, &wfds);
				}
				
			} else if (FD_ISSET(i, &wset)) {
				
				ret = send(i, buffer, ret, 0); // 
				
				FD_CLR(i, &wfds); //
				FD_SET(i, &rfds);
			}

		}

	}
	
	return 0;
}

步骤:
1、定义io管理状态变量:fd_set rfds,wfds;
2、初始化变量:FD_ZERO();
3、设置io流状态,最初只有监听的fd,将其设置:FD_SET(listenfd,rfds);
4、在循环中select
5、FD_ISSET()判断端口是否有连接
6、FD_ISSET()判断可读、可写状态

总结

本文通过对Linux网络IO和select的详细讨论,帮助读者深入理解了这些关键概念,并展示了select函数在构建高效网络应用中的重要性和灵活性。对于想要提升网络编程技能的开发者来说,这些知识将会是宝贵的参考和实践指南。

  1. 网络IO的重要性:理解网络IO是构建高效网络应用的基础。通过有效管理数据的输入和输出,可以实现更好的性能和可伸缩性。

  2. Linux中的网络IO模型:介绍了阻塞IO、非阻塞IO、多路复用IO和异步IO等不同的网络IO模型。特别地,我们重点讨论了多路复用IO模型中的select函数。

  3. select函数的作用:select函数是一种常用的多路复用机制,它可以同时监视多个文件描述符的状态变化,并通知应用程序哪些描述符可以进行读写操作。

  4. 使用select函数的优势:通过使用select函数,可以在一个线程内管理多个连接,减少了线程创建和销毁的开销,提升了系统的性能和资源利用率。

  5. select函数的工作原理:详细解释了select函数的工作原理,包括文件描述符集合的准备、调用select函数并处理返回结果的流程。

  6. select函数的限制:虽然select函数具有一定的优点,但也存在一些限制,如最大文件描述符数量的限制,每次调用都需要遍历整个描述符集合等。

  7. select函数的应用示例:通过一个实际的案例,演示了如何使用select函数实现多个TCP连接的并发处理,展示了其在网络编程中的具体应用。

image.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。