基于TCP协议的select多路复用IO服务器编程

发布于:2023-01-21 ⋅ 阅读:(419) ⋅ 点赞:(0)

一、初识TCP协议

TCP协议是面向链接,可靠的,基于字节流传输层协议 使用严格的应答机制来保证可靠性。

1、建立连接时进行三次握手

2、断开连接时进行四次挥手

3、 每次发送数据后 ,都必须要应答
4、会给每个数据包编号 , 应答时区分编号
TCP建立连接和断开连接要进行三次握手,四次挥手:

 TCP服务端的创建流程:

1、建立 socket,SOCK_STREAM

 2:绑定ip地址与端口号

 3:建立监听队列,让套接字进入到被动监听状态:int listen(int sockfd, int backlog);

 4 : 接受连接,产生新的套接字:

int accept(int sockfd, struct sockaddr * addr, socklen_t *addrlen);

 5 : 数据读写

1)发送:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

 

 2)接收 : 

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

 二、并发服务器

问题:多个客户端需要处理,请求可能同时到来 

解决办法:采用并发服务器。思想:多任务处理机制(多线程或者多进程), 分别为每个客户端创建一个任务来处理,极大的 提高了服务器的并发处理能力。

1、IO多路复用 (IO multiplexing):用单个进程对多个IO进行操作

 三、select函数

允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

1、select 函数声明如下

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
​
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

原理:

1)将需要进行io的文件描述符添加到文件描述符集合

2)将这个文件描述符集合拷贝到内核中

3)内核需要遍历文件描述符集合,循环检测对应的文件描述符是否可以进行io操作(可读 or 可写)

2、参数说明:

1)nfds:int类型,指集合中所有文件描述符的范围。即所有文件描述符的最大值加1(maxfd + 1)。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。

2)readfds:首先需要明白,fd_set是什么数据类型,有一点像int,又有点像struct,其实,fd_set声明的是一个集合,也就是说,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读。

3)writefds:和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写。

4)fd_set *exceptfds:其他文件描述符集合

5)timeout:可以用select来做超时处理。可以选择阻塞,可以选择非阻塞,还可以选择定时返回。当将timeout置为NULL时,表明此时select是阻塞的;当将tineout设置为timeout->tv_sec = 0,timeout->tv_usec = 0时,表明这个函数为非阻塞;当将timeout设置为非0的时间,表明select有超时时间,当这个时间走完,select函数就会返回。

 struct timeval{      
​
                long tv_sec;   /*秒 */
​
                long tv_usec;  /*微秒 */   
​
            }

四、select函数编写TCP服务器

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>

#define BACKLOG 10

	   /*struct timeval {
	       long    tv_sec;	       
	       long    tv_usec;       
	   };*/ //时间结构体
int main(int argc, char **argv)
{
	int i,ret,nmfds;
	int sfd,cfd,maxfd;
	fd_set rdfds,tmpfds;//原始表单,备份表单
	struct timeval tim1,tim2;//原始时间,备份时间
	ssize_t recv_bytes, send_bytes;
	char buf[128] = {0};
	struct sockaddr_in src,cli;
	socklen_t len = sizeof(src);
	socklen_t addrlen = sizeof(cli);
	sfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字,第二个参数为流式套接字类型
	if(sfd == -1)
	{
		perror("socket fail");
		return -1;
	}
	
	src.sin_family = AF_INET;//服务器地址结构体填充,地址族,IP PORT
	src.sin_port = htons(atoi(argv[2]));
	src.sin_addr.s_addr = inet_addr(argv[1]);
	ret = bind(sfd, (const struct sockaddr *)&src, len );//绑定套接字,服务器ip和port口号
	if(ret == -1)
	{
		perror("bind");
		return -1;
	}
	ret = listen(sfd, BACKLOG);//将套接字设定为被动监听状态,监听客户端的连接请求,10为未决队列长度
	if(ret == -1)
	{
		perror("listen");
		return -1;
	}
	
	FD_ZERO(&rdfds);//将2个表单清0
	FD_ZERO(&tmpfds);
	FD_SET(sfd, &rdfds);//将监听套接字加入表单
	tim1.tv_sec = 3;//超时时间设置为3s
	tim1.tv_usec = 0;
	maxfd = sfd;//最大的文件描述符设置为sfd

	while(1)
	{
		tmpfds = rdfds;//将原始表单赋值给备份表单
		tim2 = tim1;//将原始时间赋值给备份时间
		nmfds = select(maxfd + 1, &tmpfds, NULL, NULL, &tim2);
		if(nmfds == -1)
		{
			perror("select fail");
			return -1;
		}else if(nmfds == 0)//超时处理
		{
			printf("timeout\n");
		}else//有文件描述符就绪
		{
			for(i = 0; i <= maxfd; i++)//遍历所有关注的文件描述符
			{
				if(FD_ISSET(i, &tmpfds))//判断文件描述符是否就绪
				{
					if(i == sfd)//监听套接字就绪
					{
						
						cfd = accept(sfd, (struct sockaddr *)&cli,&addrlen);//接收客户端连接请求,产生连接套接字,用于和客户端通信,通过cli保存客户端ip地址和port口号
						if(cfd == -1)
						{
							perror("accept");
							return -1;
						}
						printf("client IP: %s  PORT: %d\n", inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));//打印客户端IP 和 PORT ,需要将网络字节序转换为主机字节序	
						
						FD_SET(cfd, &rdfds);//将新产生的连接套接字加入到原始表单
						maxfd = (cfd > maxfd)? cfd:maxfd;//更新最大的文件描述符
					}else if(i > sfd)//连接套接字就绪
					{
						
						memset(buf,0,sizeof(buf));
						recv_bytes = recv(i, buf, sizeof(buf),0);//接收客户端的信息储存到buf中
						if(recv_bytes == -1)
						{
							perror("recv");
							return -1;
						}else if(recv_bytes == 0)//客户端退出返回0
						{
							printf("client shutdown\n");
							close(i);//关闭相应的文件描述符
							FD_CLR(i, &rdfds);//将文件描述符清除出原始表单
						}else
						{
							printf("buf: %s\n",buf);
						}
					
					}
				
				}
			
			}
		
		}
	
	}

	return 0;
}

五、TCP客户端

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>


int main(int argc, char **argv)
{
	int sockfd;
	int ret;
	ssize_t send_bytes,recv_bytes;
	char buf[128] = {0};
	char buf1[128] = {0};
	struct sockaddr_in dest;
	socklen_t len = sizeof(dest);
	if(argc < 3)
	{
		fprintf(stderr, "usage: %s  [IP]  [PORT]\n",argv[0]);
		return -1;
	}
	sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
	if(sockfd == -1)
	{
		perror("");
		return -1;
	}
	dest.sin_family = AF_INET;//填充服务器的ip地址和port口号
	dest.sin_port = htons(atoi(argv[2]));
	dest.sin_addr.s_addr = inet_addr(argv[1]);

	ret = connect(sockfd, (const struct sockaddr *)&dest, len);//与服务器建立连接
	if(ret == -1)
	{
		perror("connect fail");
		return -1;
	}

	while(1)
	{
		memset(buf, 0 ,sizeof(buf));
		memset(buf, 0 ,sizeof(buf1));
		fgets(buf, sizeof(buf),stdin);
		send_bytes = send(sockfd, buf, strlen(buf), 0);//向服务器发送数据
		if(send_bytes == -1)
		{
			perror("send ");
			return -1;
		}
		recv_bytes = recv(sockfd, buf1, sizeof(buf1),0);//接收服务器发来的数据,没有收数据会阻塞在这里
		if(recv_bytes == -1)
		{
			perror("recv ");
			return -1;
		}

	}

	return 0;
}

六、服务端运行结果


网站公告

今日签到

点亮在社区的每一天
去签到