Linux网络编程3--select

Day3

一、TCP状态转换图

在这里插入图片描述

  • 处于ESTABLISHED状态的时候就可以收发数据了, 双方在通信过程当中一直处于ESTABLISHED状态, 数据传输期间没有状态的变化.
    TIME_WAIT状态一定是出现在主动关闭的一方.
  • 主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。
  • socket-pair的概念: 客户端与服务端连接其实是一个连接对, 可以通过使用netstat -anp | grep 端口号 进行查看.

二、端口复用

  • 解决端口复用的问题: bind error: Address already in use, 发生这种情况是在服务端主动关闭连接以后, 接着立刻启动就会报这种错误.
  • setsockopt函数
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    例:setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

三、心跳包

如何检查与对方的网络连接是否正常?一般心跳包用于长连接.
在这里插入图片描述
方法:在应用程序中自己定义心跳包, 使用灵活, 能实时把控.

四、高并发服务器模型–select

  • 多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理。
  • 数据类型fd_set: 文件描述符集合–本质是位图(关于集合可联想一个信号集sigset_t)。
  • select函数介绍
      int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生.
    函数说明:
    nfds: 最大的文件描述符+1
    readfds: 读集合, 是一个传入传出参数
      传入: 指的是告诉内核哪些文件描述符需要监控
      传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
    writefds: 写文件描述符集合(传入传出参数)
    execptfds: 异常文件描述符集合(传入传出参数)
    timeout:
      NULL–表示永久阻塞, 直到有事件发生
      0–表示不阻塞, 立刻返回, 不管是否有监控的事件发生
      >0–到指定事件或者有事件发生了就返回
  • FD_函数介绍
    void FD_CLR(int fd, fd_set *set);
      将fd从set集合中清除.
    int FD_ISSET(int fd, fd_set *set);
      功能描述: 判断fd是否在集合中
      返回值: 如果fd在set集合中, 返回1, 否则返回0.
    void FD_SET(int fd, fd_set *set);
      将fd设置到set集合中.
    void FD_ZERO(fd_set *set);
      初始化set集合.
    调用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生;
  • 代码思路
使用select的开发服务端流程:
1 创建socket, 得到监听文件描述符lfd---socket()
2 设置端口复用-----setsockopt()
3 将lfd和IP  PORT绑定----bind()
4 设置监听---listen()
5 fd_set readfds;  //定义文件描述符集变量
  fd_set tmpfds;
  FD_ZERO(&readfds);  //清空文件描述符集变量
  FD_SET(lfd, &readfds);//将lfd加入到readfds集合中;
  maxfd = lfd;
  while(1)
  {
  	tmpfds = readfds;
  	nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
  	if(nready<0) 
  	{ 
  		if(errno==EINTR)//被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	//有客户端连接请求到来
  	if(FD_ISSET(lfd, &tmpfds))
  	{
  		//接受新的客户端连接请求
  		cfd = accept(lfd, NULL, NULL);
  		
  		//将cfd加入到readfds集合中
  		FD_SET(cfd, &readfds);
  		
  		//修改内核监控的文件描述符的范围
  		if(maxfd<cfd)
  		{
  			maxfd = cfd;
  		}
  		
  		if(--nready==0)
  		{
  			continue;
  		}
  	}
  	
  	
  	//有客户端数据发来
  	for(i=lfd+1; i<=maxfd; i++)
  	{
  		if(FD_ISSET(i, &tmpfds))
  		{
			//read数据
  			n = read(i, buf, sizeof(buf));
  			if(n<=0)
  			{
  				close(i);
  				//将文件描述符i从内核中去除
  				FD_CLR(i, &readfds);
  			}
  			
  			//write应答数据给客户端
  			write(i, buf, n);
  		}
  		
		if(--nready==0)
  		{
  			break;
  		}
  	}
  	
  	close(lfd);
  	
  	return 0;
  }
  • 代码实现
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include "wrap.h"
#include <ctype.h>

int main()
{
    int lfd;
    //创建socket
    lfd = Socket(AF_INET, SOCK_STREAM, 0);
    
    //设置端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

    //绑定
    struct sockaddr_in serv;
    serv.sin_family = AF_INET;
    serv.sin_port = htons(8888);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));

    //监听
    Listen(lfd, 128);

    //定义fd_set类型的变量
    fd_set readfds;
    fd_set tmpfds;

    //清空readfds和tmpfds
    FD_ZERO(&readfds);
    FD_ZERO(&tmpfds);

    //将lfd加入到readfds中,委托内核监控
    FD_SET(lfd, &readfds);

    int nready;
    int maxfd = lfd;
    int cfd;
    int i;
    int sockfd;
    char buf[1024];
    int n;

    while(1)
    {
        tmpfds = readfds;
        //tmpfds是输入输出参数
        //输入:告诉内核要监控那些文件描述符
        //输出:内核告诉应用程序有那些文件描述符发生了变化
        nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
        if(nready < 0) 
        {
            if(errno == EINTR)//被信号中断
            {
                continue;              
            }
            break;
        }
		printf("nready==[%d]\n", nready);

		//有客户端连接请求到来
		if(FD_ISSET(lfd, &tmpfds))
		{
			//接受新的客户端连接请求
			cfd = Accept(lfd, NULL, NULL);

			//将cfd加入到readfds集合中
			FD_SET(cfd, &readfds);

			//修改内核监控范围
			if(maxfd < cfd)
			{
				maxfd = cfd;                    
			}

			if(--nready==0)
			{
				continue;
			}
		}

		//有数据发来的情况
		for(i = lfd + 1; i<=maxfd; i++)
		{
			sockfd = i;
			//判断sockfd文件描述符是否有变化
			if(FD_ISSET(sockfd, &tmpfds))
			{
				//读数据
				memset(buf, 0x00, sizeof(buf));
				n = Read(sockfd, buf, sizeof(buf));
				if(n <= 0)
				{
					//关闭连接
					close(sockfd);

					//将sockfd从readfds中删除
					FD_CLR(sockfd, &readfds);
				}
				else
				{
					printf("n == [%d], buf == [%s]\n", n, buf);

					int k = 0;
					for(k = 0; k < n; k++)
					{
						buf[k] = toupper(buf[k]);
					}
					Write(sockfd, buf, n);
				}

				if(--nready==0)
				{
					break;
				}
			}
		}
	}
	
	
	close(lfd);
	return 0;	
}
  • 代码优化
//IO多路复用技术select函数的使用 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

int main()
{
	int i;
	int n;
	int lfd;
	int cfd;
	int ret;
	int nready;
	int maxfd;//最大的文件描述符
	char buf[FD_SETSIZE];
	socklen_t len;
	int maxi;  //有效的文件描述符最大值
	int connfd[FD_SETSIZE]; //有效的文件描述符数组
	fd_set tmpfds, rdfds; //要监控的文件描述符集
	struct sockaddr_in svraddr, cliaddr;

	//创建socket
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	if(lfd<0)
	{
		perror("socket error");
		return -1;
	}

	//允许端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	ret = bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	if(ret<0)
	{
		perror("bind error");
		return -1;
	}

	//监听listen
	ret = listen(lfd, 5);
	if(ret<0)
	{
		perror("listen error");
		return -1;
	}

	//文件描述符集初始化
	FD_ZERO(&tmpfds);
	FD_ZERO(&rdfds);

	//将lfd加入到监控的读集合中
	FD_SET(lfd, &rdfds);

	//初始化有效的文件描述符集, 为-1表示可用, 该数组不保存lfd
	for(i=0; i<FD_SETSIZE; i++)
	{
		connfd[i] = -1;
	}

	maxfd = lfd;
	len = sizeof(struct sockaddr_in);

	//将监听文件描述符lfd加入到select监控中
	while(1)
	{
		//select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回
		//select的第二个参数tmpfds为输入输出参数,调用select完毕后这个集合中保留的是发生变化的文件描述符
		tmpfds = rdfds;
		nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
		if(nready>0)
		{
			//发生变化的文件描述符有两类, 一类是监听的, 一类是用于数据通信的
			//监听文件描述符有变化, 有新的连接到来, 则accept新的连接
			if(FD_ISSET(lfd, &tmpfds))	
			{
				cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);			
				if(cfd<0)
				{
					if(errno==ECONNABORTED || errno==EINTR)
					{
						continue;
					}
					break;
				}

				//先找位置, 然后将新的连接的文件描述符保存到connfd数组中
				for(i=0; i<FD_SETSIZE; i++)
				{
					if(connfd[i]==-1)
					{
						connfd[i] = cfd;
						break;
					}
				}
				//若连接总数达到了最大值,则关闭该连接
				if(i==FD_SETSIZE)
				{	
					close(cfd);
					printf("too many clients, i==[%d]\n", i);
					//exit(1);//服务器进程不轻易退出
					continue;
				}

				//确保connfd中maxi保存的是最后一个文件描述符的下标
				if(i>maxi)
				{
					maxi = i;
				}

				//打印客户端的IP和PORT
				char sIP[16];
				memset(sIP, 0x00, sizeof(sIP));
				printf("receive from client--->IP[%s],PORT:[%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, sIP, sizeof(sIP)), htons(cliaddr.sin_port));

				//将新的文件 描述符加入到select监控的文件描述符集合中
				FD_SET(cfd, &rdfds);
				if(maxfd<cfd)
				{
					maxfd = cfd;
				}

				//若没有变化的文件描述符,则无需执行后续代码
				if(--nready<=0)
				{
					continue;
				}	
			}

			//下面是通信的文件描述符有变化的情况
			//只需循环connfd数组中有效的文件描述符即可, 这样可以减少循环的次数
			for(i=0; i<=maxi; i++)
			{
				int sockfd = connfd[i];
				//数组内的文件描述符如果被释放有可能变成-1
				if(sockfd==-1)
				{
					continue;
				}

				if(FD_ISSET(sockfd, &tmpfds))
				{
					memset(buf, 0x00, sizeof(buf));
					n = read(sockfd, buf, sizeof(buf));
					if(n<0)
					{
						perror("read over");
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else if(n==0)
					{
						printf("client is closed\n");	
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else
					{
						printf("[%d]:[%s]\n", n, buf);
						write(sockfd, buf, n);
					}

					if(--nready<=0)
					{
						break;  //注意这里是break,而不是continue, 应该是从最外层的while继续循环
					}
				}	
			}
		}	
	}

	//关闭监听文件描述符
	close(lfd);

	return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值