Linux网络学习 第三天

目录

内容回顾

学习目标

TCP状态转换图

端口复用

半关闭状态

心跳包

高并发服务器模型--select


内容回顾

当read读文件描述符为非阻塞状态的时候, 若对方没有发送数据, 会立刻返回, errno设置为EAGAIN, 这个错误我们要忽略.

学习目标

  • 熟练掌握TCP状态转换图
  • 熟练掌握端口复用的方法
  • 了解半关闭的概念和实现方式
  • 了解多路IO转接模型
  • 熟练掌握select函数的使用
  • 熟练使用fd_set相关函数的使用
  • 能够编写select多路IO转接模型的代码

TCP状态转换图

  了解TCP状态转换图可以帮助开发人员查找问题.

说明: 上图中粗线表示主动方, 虚线表示被动方, 细线部分表示一些特殊情况, 了解即可, 不必深入研究.

       对于建立连接的过程客户端属于主动方, 服务端属于被动接受方(图的上半部分)

而对于关闭(图的下半部分), 服务端和客户端都可以先进行关闭.

处于ESTABLISHED状态的时候就可以收发数据了, 双方在通信过程当中一直处于ESTABLISHED状态, 数据传输期间没有状态的变化.

TIME_WAIT状态一定是出现在主动关闭的一方.

主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。

使用netstat -anp可以查看连接状态

:数据传输的时候带了一个字节的数据, 所以server发送给clientACK=x+2

思考题:

1 SYN_SENT状态出现在哪一方? 客户端

2 SYN_RCVD状态出现在哪一方? 服务端

3 TIME_WAIT状态出现在哪一方?  主动关闭方

4 在数据传输的时候没有状态变化.

TIME_WAIT是如何出现的:

       启动服务端, 启动客户端, 连接建好, 而且也可以正常发送数据;

       然后先关闭服务端, 服务端就会出现TIME_WAIT状态.

2MSL相当于一个时间段。

为什么需要2MSL?

       原因之一: 让四次挥手的过程更可靠, 确保最后一个发送给对方的ACK到达;

若对方没有收到ACK应答, 对方会再次发送FIN请求关闭, 此时在2MS时间内被动关闭方仍然可以发送ACK给对方.

      

原因之二: 为了保证在2MS时间内, 不能启动相同的SOCKET-PAIR.

              TIME_WAIT一定是出现在主动关闭的一方, 也就是说2MS是针对主动关         闭一方来说的;由于TCP有可能存在丢包重传, 丢包重传若发给了已经断         开连接之后相同的socket-pair(该连接是新建的, 与原来的socket-pair完          全相同, 双方使用的是相同的IP和端口), 这样会对之后的连接造成困扰,         严重可能引起程序异常.

       如何避免问题2呢??

              --很多操作系统实现的时候, 只要端口被占用, 服务就不能启动.

测试: 启动服务端和客户端, 然后先关闭服务端, 再次启动服务端, 此时服务端报错: bind error: Address already in use; 若是先关闭的客户端, 再关闭的服务端, 此时启动服务端就不会报这个错误.

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));
	setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int));

函数说明可参看<<UNIX环境高级编程>>

由于错误是bind函数报出来的, 该函数调用要放在bind之前, socket之后调用.

半关闭状态

半关闭的概念:

       如果一方close, 另一方没有close, 则认为是半关闭状态, 处于半关闭状态的 时候, 可以接收数据, 但是不能发送数据. 相当于把文件描述符的写缓冲区 操作关闭了.

       注意: 半关闭一定是出现在主动关闭的一方.

shutdown函数

       创建连接需要进行三次握手,需要花费时间。

       长连接和端连接的概念:

              连接建立之后一直不关闭为长连接;

              连接收发数据完毕之后就关闭为短连接;

shutdown和close的区别:

1 shutdown可以实现半关闭, close不行

2 shutdown关闭的时候, 不考虑文件描述符的引用计数, 是直接彻底关闭

close考虑文件描述符的引用计数, 调用一次close只是将引用计数减1,

只有减小到0的时候才会真正关闭.(子进程复制的时候会增加,只有全部关闭才能彻底关闭)内核还是可以读写,只是设置的客户端或服务器不能进行相应的读或写

shutdown能够把文件描述符上的读或者写操作关闭, 而close关闭文件描述  符只是将连接的引用计数的值减1, 当减到0就真正关闭文件描述符了.

如: 调用dup函数或者dup2函数可以复制一个文件描述符, close其中一个并不影响另一个文件描述符, 而shutdown就不同了, 一旦shutdown了其中一  个文件描述符, 对所有的文件描述符都有影响 .

心跳包

如何检查与对方的网络连接是否正常??

一般心跳包用于长连接.

方法1

keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

由于不能实时的检测网络情况, 一般不用这种方法

方法2: 在应用程序中自己定义心跳包, 使用灵活, 能实时把控.

到此为止, 概念相关的东西就讲完毕了.

什么是心跳包?

用于监测长连接是否正常的字符串.

在什么情况下使用心跳包?

       主要用于监测长连接是否正常.

如何使用心跳包?

       通信双方需要协商规则(协议), 如4个字节长度+数据部分

高并发服务器模型--select

继续研究高并发服务器的问题.

多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理,

数据类型fd_set: 文件描述符集合--本质是位图(关于集合可联想一个信号集sigset_t)

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--到指定事件或者有事件发生了就返回,若超过时常,则立刻返回
	
返回值: 成功返回发生变化的文件描述符的个数
		失败返回-1, 并设置errno值.
*/

/usr/include/x86_64-linux-gnu/sys/select.h和

/usr/include/x86_64-linux-gnu/bits/select.h

从上面的文件中可以看出, 这几个宏本质上还是位操作.

//将fd从set集合中清除.
void FD_CLR(int fd, fd_set *set);

//功能描述: 判断fd是否在集合中
//返回值: 如果fd在set集合中, 返回1, 否则返回0.
int FD_ISSET(int fd, fd_set *set);

//将fd设置到set集合中.
void FD_SET(int fd, fd_set *set);

//初始化set集合.
void FD_ZERO(fd_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 <arpa/inet.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <sys/wait.h>
#include "warp.h"

//父子进程中共享的内容
//文件描述符
//mmap映射区

int main()
{
    //创建socket
    int lfd = Socket(AF_INET, SOCK_STREAM, 0);

    //设置端口复用 防止服务器断开时不能马上启动 两种写法
    //端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
    //setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int)); 

    //绑定数据
    struct sockaddr_in serv;
    bzero(&serv, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(8888);    //host代表主机 s短整型 l长整型 只能绑定未使用的端口 否则 报错bind error: Permission denied
    serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用IP
    if((Bind(lfd, (struct sockaddr *)&serv, sizeof(serv))) < 0)
    {
        return -1;
    }

    //监听数据
    Listen(lfd, 128);

    //定义文件描述符变量
    fd_set readfds;

    //零时变量保存select读取的内核中的文件描述符集 如果使用readfds传入引用会对其进行修改
    fd_set tmpfds;

    //清空文件描述符 初始化
    FD_ZERO(&readfds);
    FD_ZERO(&tmpfds);

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

    int maxfd = lfd;
    int nready;
    int cfd;
    int curfd;
    int n;
    char buf[256];
    while(1)
    {
        //tmpfds是输入输出函数
        //输入:告诉内核要检测那些文件描述符
        //输出:内核告诉应用程序有那些文件描述符发生改变
        //select只能一个一个执行请求 如果想要同时处理 可以使用多线程或者多进程 这样效率比单独使用多进程要高
        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(curfd = lfd + 1; curfd <= maxfd; curfd++)
        {
            //判断curfd是否发生变化
            if(FD_ISSET(curfd, &tmpfds))
            {
                //read数据
                memset(buf, 0x00, sizeof(buf));
                n = read(curfd, buf, sizeof(buf));
                if(n<=0)
                {
                    //关闭连接
                    close(curfd);

                    //将文件描述符i从内核中去除
                    FD_CLR(curfd, &readfds);
                    break;
                }
                else
                {
                    //获取数据
                    printf("buf = [%s], length = [%d]\n", buf, n);
                    for(int k = 0; k < n; k++)
                    {
                        buf[k] = toupper(buf[k]);
                    }

                    write(curfd, buf, n);
                }

                //客户端不存在时 减少循环次数
                //判断位置很重要,需要写在FD_ISSET(curfd, &tmpfds)里面
                //nready返回的是变化的文件描述符个数 如果写在外面会进入死循环
                //写在外面如果改位置的cfd没有发生变化 nready也会进行减 最大连接数没有改变 因此会一直循环
                //该判断也可以不写 写了是为了提升效率 减少不必要的循环
                // 0 0 1 0 如果循环到了第三个 直接会退出循环
                if(--nready == 0)
                {
                    break;//注意这里是break,而不是continue, 应该是从最外层的while继续循环
                }
            }
        }
    }

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

    return 0;
}

可以使用发生事件的总数进行控制, 减少循环次数

 调用select函数涉及到了用户空间和内核空间的数值交互过程.

 事件一共包括两部分, 一类是新连接事件, 一类是有数据可读的事件

问题分析: select函数的readfds是一个传出传入参数

关于select的思考:

       问题: 如果有效的文件描述符比较少, 会使循环的次数太多.

       解决办法: 可以将有效的文件描述符放到一个数组当中, 这样遍历效率就高了.

select优点:

       1 一个进程可以支持多个客户端

       2 select支持跨平台

select缺点:

       1 代码编写困难

       2 会涉及到用户区到内核区的来回拷贝

       3 当客户端多个连接, 但少数活跃的情况, select效率较低

例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下

       4 最大支持1024个客户端连接

select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的, 而是由FD_SETSIZE=1024限制的.

FD_SETSIZE=1024  fd_set使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做.

作业:

       编写代码, 让select监控标准输入, 监控网络, 如果标准输入有数据就写入网络, 如果网络有数据就读出网络数据, 然后打印到标准输出.

注意: select不仅可以监控socket文件描述符, 也可以监视标准输入.

代码优化方向:

int client[1024]

for()

{

       client[i] = -1;

}

优化的代码如下:

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

1 将通信文件描述符保存到一个整形数组中, 使用一个变量记录

  数组中最大元素的下标maxi.

2 如果数组中有无效的文件描述符, 直接跳过

POSIX表示可移植操作系统接口Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准.

关于fd_set类型的底层定义:

/usr/include/x86_64-linux-gnu/sys/select.h和

/usr/include/x86_64-linux-gnu/bits/select.h

/usr/include/x86_64-linux-gnu/sys/select.h文件中:

 __NFDBITS计算出来的值是: 8*8=64

上面是在头文件中一步一步跟踪的定义, 最简单的方法就是使用预处理将头文件和宏全部替换掉, 直接就可以看到最终的定义了.

如: gcc -E select.c -o select.i

打开select.i后

typedef struct

  {

    __fd_mask __fds_bits[1024 / (8 * (int) sizeof (__fd_mask))];

  } fd_set;

进一步转换后:

typedef struct

{

long int __fds_bits[1024/(8*8))];

//long int __fds_bits[16];

}         

这个数组一共占用: 8 * 16 * 8 = 1024, 也就是说fd_set这个文件描述符表中一共有1024个bit位, 每个bit位只有0和1两种值, 1表示有, 0表示没有.

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值