linux网络编程 2019.1.12(send/recv,TCP状态转化,shutdown,netstat命令,端口复用, IO多路转接,select, poll)

 

 

复习三次握手四次挥手

       

 

 

 

send/recv

recv和sendread和write多一个参数flag,见到flag的时候,就把flag赋值为0.(没讲为啥。。。)

 

 

 

TCP状态转化

 

三次握手

客户端收到服务器发送的SYN+ACK的时候,客户端的状态就变成了ESTABLISHED,当进入该状态的时候,说明该端可以发送数据进行通信了。

 

四次挥手

谁先关闭连接谁的状态先发生变化

 

TCP状态转换图

 

分析:那些状态能捕捉到,那些捕捉不到(假定客户端发起断开)

捕捉不到

客户端

  • SYN_SENT
  • FIN_WAIT_1

服务端

  • SYN_RCVD
  • CLOSE_WAIT
  • LAST_ACK

 

捕捉的到

客户端

  • ESTABLISHED
  • FIN_WAIT2
  • TIME_WAIT

服务端

  • ESTABLISHED

 

2MSL

MSL一般是30秒,2MSL的时间是1min。

主动断开连接的一方要等待2MSL时间,然后结束进程。

等待这段时间是为了对端能正常的销毁进程,为保证对端能收到ack,如果对端没有收到发起断开的一方发送的最后一个ack时,2MSL可以补发ack。为什么会补发ack?因为对端没收到ack就会补发FIN

 

 

半关闭

函数 shutdown

int shutdown(int sockfd, int how);
  • sockfd: 要半关闭的一方对应的文件描述符

使用方法

  • SHUT_RD  - 0  - 读
  • SHUT_WR - 1- 写
  • SHUT_RDWR  -2  - 读写

使用shutdown之后,不管你赋值了多少文件描述符,操作了一个,所有的就都失效了

(复制文件描述符使用dup2函数,new指向old的那一块内存空间,,浅拷贝了目测)

 

 

 

netstat命令

可以捕捉到相应的进程状态。

 

例子——查看监听8888端口的进程

netstat -apn | grep 8888

 

 

 

 

端口复用设置

当服务器进程端口连接再重启,会显示以下返回值:

因为进入time wait状态了。

 

端口复用的用途

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出系统没有释放端口

 

设置方法:

int opt = 1;    
setsockopt(sockfd, SOL_SOCKET, 
    SO_REUSEADDR,
     (const void *)&opt, sizeof(opt)); 

端口复用只是该函数的一个功能。

man手册截图(这俩函数在unp第七章)

 

端口复用函数要设置在绑定之前

int main(int argc, const char* argv[])
{
    // 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // lfd 和本地的IP port绑定
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;    // 地址族协议 - ipv4
    server.sin_port = htons(8888);
    server.sin_addr.s_addr = htonl(INADDR_ANY);

    //设置端口复用
    int flag = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));

    int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    ...

}

 

 

 

 

IO多路转接

 

IO操作方式

1、阻塞等待

  • 好处——不占用cpu时间片
  • 缺点——同一时刻只能处理一个操作,效率低。

2、非阻塞,忙轮询

  • 优点——提高程序的执行效率
  • 缺点——消耗CPU

 

解决方案——使用IO多路转接技术select/poll/epoll

IO多路转接,我们需要委托内核帮我们做一些事情、委托内核告诉我们,连接我们的客户端中,有哪些客户端与我们进行通信

第一种:select/poll——遍历线性表来检测谁跟我们通信了

只会告诉我有几个客户端与我们通信了,分别是谁,我们不知道。我们需要把所有连接的客户端去进行遍历,找出都有谁跟我通信了。

第二种:epoll——遍历红黑树来检测谁跟我们通信了

不仅告诉我们几个给我们通信了,还会告诉我们跟我们通信的客户端都是谁。

 

什么是I/O多路转接技术:

内核需要检测与文件描述符有关的表,检测文件描述符缓冲区,因为读缓冲区有东西则证明有人发数据过来了。

 

      

 

select的参数和返回值

 

函数原型:——返回值是文件描述符中有几个发生了变化,是int类型

 

参数

  • nfds:  要检测的文件描述中最大的fd+1 ——nfds的最大值是1024,nfds在内核中用数组来实现的
  • readfds: 集合
  • writefds:  集合
  • exceptfds: 异常集合
  • timeout:  阻塞时间

 

文件描述符集类型:

fd_set  rdset;

 

文件描述符操作函数:(函数的使用范例在下方图片中绿色字所示)重点

  • 全部清空
    • void FD_ZERO(fd_set *set);
  • 从集合中删除某一项
    • void FD_CLR(int fd, fd_set *set);
  • 将某个文件描述符添加到集合
    •  void FD_SET(int fd, fd_set *set);
  • 判断某个文件描述符是否在集合中
    • int  FD_ISSET(int fd, fd_set *set);

 

使用select函的优缺点:

  • 优点:
    • 跨平台
  • 缺点:
    • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
    • select支持的文件描述符数量太小了,默认是1024

 

 

为什么默认描述符数量是1024?

下图所示是fd_set的内核代码片段截图,通过截图可以看到fd_set实际上是_kernel_fd_set结构体,而结构体里面只有一个数组,因此fd_set实际上是一个数组,数组的长度是__FDSET_LONGS,查看__FDSET_LONGS的定义可知,他的大小等于定义出的FD_SETSIZE宏的大小是1024_NFDBITS的大小是

算一下,1024/(8*(32位则unsigned long的值为4, 64位则unsigned long的值为8,我们假定是64位))=16=__FDSET_LONGS,因此数组大小是16,类型是long,64位系统中long是8字节,每个字节8位,

因此fd_set的二进制标志位数量就是:16*8*8=1024

 

 

fd_set的内核代码片段截图

因为selete的局限性是只能委托1024个。

 

当被监听的描述符发生变化,就会返回。

 

 

 

select函数是如何工作的

假定客户端A,B,C,D,E, F连接到服务器,分别对应文件描述符 3, 4,100,101,102,103

因此我们要通过调用select函数通过内核来判断链接上来的客户端是否给我发送数据了,

 

第一步——创建一个fd_set类型的表

fd_set reads, temp; - 用户空间   
//temp用于作为select传入的参数,因为select函数会把传入的参数的值修改掉,具体原因往下看

 

第二步——文件描述符添加到待检测的表中

FD_SET(3, &reads);
 FD_SET(4, &reads);

以此类推,全都写一遍

 

第三步——调用select函数

select(103+1   //最大的文件描述符+1
       &reads, //读集合中的地址
       NULL,   //写
       NULL,   //异常
       NULL    //时延
)

调用函数之后,内核就开始干活了。

内核 先把参数reads的内容从用户空间复制一份表到内核空间,然后依次遍历复制过来的表,如果该二进制位的值为1,说面该标志位是一个客户端连接的文件描述符,则去查看内核缓冲区的read空间中是否有数据,如果有数据,则该位置的值不变,如果没有数据,则将该位置的值置为0.(内核检测完成之后,修改了fd_set表)

比如ABC发送了数据,则

修改之后,这份表又会从内核区拷贝到用户区,新拷贝过来的表会覆盖我的 &reads ,因此调用select的时候,传入的 &reads 应该是原始数据的拷贝

我们再去遍历 &reads 就能找到哪些客户端发送了数据给我,select函数的返回值只是告诉你有几个客户端发送数据。

 

 

 

程序中使用select函数的伪代码

select的io转接是异步的。

通过下面伪代码可以看出,当判断完新连接之后,要在下一轮while循环中才看去读取客户端是否给我发送数据。当我们获知新的链接的同时,客户端给我们发送了数据我们是无法实时的知道的,客户端发过来的数据此时还暂时的存储在内核read缓冲区中。

int main(){
	int lfd = socket();//创建一个用于监听的套接字
	bind();         //绑定
	listen();       //监听
	
	// 创建一文件描述符表
	fd_st reads, temp;
	// 初始化
	fd_zero(&reads);
	// 监听的lfd加入到读集合
	//监听的文件描述符不负责数据发送,那么他如何知道别人跟他建立链接了?
	//listen函数处理SYN,这也算是收发数据。如果有连接请求,就会发到文件描述符的读缓冲区了,
        //因此监听的文件描述符也需要内核帮我们去检测的。如果有新的连接,则会有数据
	fd_set(lfd, &reads);
	//获取最大的文件描述符
	int maxfd = lfd;
	
	while(1){
		// 委托内核检测
		//拷贝文件描述符表
		temp = reads;
		//调用select函数,获取有几个客户端发送了数据给我
		int ret = select(maxfd+1, &temp, NULL, NULL, NULL);
		
		// 判断监听描述符的标志位是否为1,如果是1,则说明有新连接
		if(fd_isset(lfd, &temp)){
			// 接受新连接(此时accept不会阻塞,因为进入这个if,说明真的有新连接)
			int cfd = accept();
			// cfd加入我们要检测的读集合
			//读的是reads,因为reads存的是原始数据
			fd_set(cfd, &reads);
			// 新来的客户端连接的文件描述符有可能会是最大的
			//因此要更新记录最大文件描述符的变量值maxfd
			maxfd=maxfd<cfd ? cfd:maxfd;
		}
		
		// 不是有新连接,说明此时在通信
		//遍历判断哪个文件描述符绑定的客户端发送了数据
		for(int i=lfd+1; i<=maxfd;++i){
			if(fd_isset(i, &temp){
				int len = read();
				//说明对方断开连接
				if(len == 0){
					// cfd 从读集合中删除
					fd_clr( i, &reads);
				}
				write();
			}
		}
	}
}

 

 

select的实现例子

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/select.h>
#include<errno.h>

int main(int argc, char *argv[]){
	if(argc<2){
		printf("eg: ./a.out port \n");
		exit(1);
	}

	struct sockaddr_in serv_addr;
	socklen_t serv_len=sizeof(serv_addr);
	int port=atoi(argv[1]);
	
	//创建套接字
	int lfd=socket(AF_INET, SOCK_STREAM, 0);
	//初始化服务器 
	memset(&serv_addr, 0, serv_len);
	//地址族
	serv_addr.sin_family=AF_INET;
	//监听本地所有IP
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	//设置端口
	serv_addr.sin_port=htons(port);
	//绑定IP和端口
	bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

	//设置同时监听的最大个数
	listen(lfd, 36);
	printf("Start accept .... \n");

	//使用信号回收子进程pcb
	//不然让父进程一直用wait循环等待,那父进程就什么都做不了了
	struct sigaction act;
	act.sa_handler=recyle;
	act.sa_flags=0;
	//初始化mask,不然局部变量里面有什么我们不确定
	sigemptyset(&act.sa_mask);
	//规定我们要接受的信号
	sigaction(SIGCHLD, &act, NULL);
	
	struct sockaddr_in client_addr;
	socklen_t cli_len=sizeof(client_addr);

	//最大文件描述符
	int maxfd=lfd;
	//文件描述符读集合
	fd_set reads, temp;
	//初始化reads为0
	FD_ZERO(&reads);
	//把lfd放在读集合里,让内核帮忙检测是否有新的连接请求
	FD_SET(lfd, &reads);
	while(1){
		//委托内核做IO检测
		temp=reads;
		//select函数是跨平台的,在linux下,第一个参数一定要写对
		//在windows下,第一个参数你写-1,写谁都行
		int ret=select(maxfd+1, &temp, NULL, NULL, NULL);
		if(ret==-1){
			perror("select error");
			exit(1);
		}

		//有客户端发起新连接
		if(FD_ISSET(lfd, &temp)){
			//返回值是1,说明有新的连接
			//接受连接请求——accept不阻塞
			int cfd=accept(lfd, 
				(struct sockaddr*)&client_addr, &cli_len);
			if(cfd==-1){
				perror("accept error");
				exit(1);
			}
			char ip[64];
			printf("new client IP:%s, PORT: %d\n",
					inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,sizeof(ip)),
					ntohs(client_addr.sin_port));
			//将cfd加入到待检测的读集合,这样下一次就能检测到了
			FD_SET(cfd, &reads);
			//更新最大描述符
			maxfd=maxfd<cfd?cfd:maxfd;
		}

		//已经连接上的客户端给我发送了数据
		//遍历read,去看
		for(int i=lfd+1; i<=maxfd; ++i){
			//判断temp,因为temp是被内核修改过的表
			if(FD_ISSET(i, &temp)){
				char buf[1024]={0};
				int len=recv(i, buf, sizeof(buf), 0);
				if(len==-1){
					perror("recv error");
					exit(1);
				}else if(len ==0){
					printf("客户端已经断开连接\n");
					close(i);
					//从读集合删除
					FD_CLR(i, &reads);
				}else{
					//正常的手法数据
					printf("recv buf: %s\n", buf);
					//把数据发出去 
					//不把\0发出去的前提条件是buf已经被初始化了
					//如果buf没初始化,则需要把\0发出去,否则接收端
					//收到的数据的后边是乱码
					send(i, buf, strlen(buf), 0);
				}
			}
		}
	}

	close(lfd);
	return 0;
}

 

 

 

poll函数

poll可以突破1024,因为他内部实现应用的是链表

select有读写异常者三种行为,三种行为做了三张表。poll把读写异常者三种状态封装到一个结构体里面,将三张表融合了。

函数原型(select的和poll的):

int select(int nfds, 
            fd_set *readfds, 
            fd_set *writefds,
            fd_set *exceptfds, 
            struct timeval *timeout);
struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 等待的事件 */
    short revents;    /* 实际发生的事件 */
};

int poll(struct pollfd *fd,  //结构体数组首地址,用数组,因为链接客户端的文件描述符不能只是一个
            nfds_t nfds,    //被typedef重新定义的数据类型,看成int类型就可以,功能和select的nfds差不多
            int timeout);
  • pollfd -- 数组的地址
  • nfds: 数组的最大长度, 数组中最后一个使用的元素下标+1
    • 内核会轮询检测fd数组的每个文件描述符
  • timeout:
    • -1: 永久阻塞
    • 0: 调用完成立即返回
    • >0: 等待的时长毫秒
  • 返回值: iO发生变化的文件描述符的个数

 

你想监听哪个事件(读写异常),就把相应的时间赋值给结构体的events变量。

 

 

如果你读写异常三个都想监听,该如何设置?

模仿open函数,利用按位或

short是两个字节,16位,我们对相应的位赋值即可,不用像select一样去弄很多表。

内核给的反馈也赋值给了结构体。

 

下表看框起来的东西就行了

 

poll的实现代码(不要求会写,会select就可以了)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h>

#define SERV_PORT 8989

int main(int argc, const char* argv[]){
    int lfd, cfd;
    struct sockaddr_in serv_addr, clien_addr;
    int serv_len, clien_len;

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(SERV_PORT);            // 设置端口 
    serv_len = sizeof(serv_addr);
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    // poll结构体
    struct pollfd allfd[1024];
	//最后一个有效元素下标
    int max_index = 0;
    // 初始化数组,fd的值为-1,说明没被占用
    for(int i=0; i<1024; ++i){
        allfd[i].fd = -1;
    }
	//把用于监听的描述符加入到待检测的数组里面
    allfd[0].fd = lfd;
	//初始化事件 POLLIN指的是读事件
	allfd[0].events = POLLIN;

    while(1){
        int i = 0;
        int ret = poll(allfd, max_index+1, -1); 
        if(ret == -1){
            perror("poll error");
            exit(1);
        }

        // 判断是否有连接请求
		//按位与操作,16位的数如果里面有POLLIN,则&的结果为1
        if(allfd[0].revents & POLLIN){
            clien_len = sizeof(clien_addr);
            // 接受连接请求
            int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
            printf("============\n");

            // cfd添加到poll数组
            for(i=0; i<1024; ++i){
                if(allfd[i].fd == -1){
                    allfd[i].fd = cfd;
                    break;
                }
            }
            // 更新最后一个元素的下标
            max_index = max_index < i ? i : max_index;
        }

        //有客户端发数据了,则遍历数组来接收数据
        for(i=1; i<=max_index; ++i){
            int fd = allfd[i].fd;
			//说明此位置是无效位置
            if(fd == -1){
                continue;
            }
            if(allfd[i].revents & POLLIN){
                // 接受数据
                char buf[1024] = {0};
                int len = recv(fd, buf, sizeof(buf), 0);
                if(len == -1){
                    perror("recv error");
                    exit(1);
                }else if(len == 0){
                    allfd[i].fd = -1;
                    close(fd);
                    printf("客户端已经主动断开连接。。。\n");
                } else {
                    printf("recv buf = %s\n", buf);
                    for(int k=0; k<len; ++k){
						//小写转大写
                        buf[k] = toupper(buf[k]);
                    }
                    printf("buf toupper: %s\n", buf);
                    send(fd, buf, strlen(buf)+1, 0);
                }
            }
        }
    }

    close(lfd);
    return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值