(二)socket编程(三)

目录

socket编程(七)

TCP 11种状态

连接建立三次握手、连接终止四次握手

TIME_WAIT与SO_REUSEADDR

RST标志

SIGPIPE

socket编程(八)

五种I/O模型

1. 阻塞I/O模型

2. 非阻塞I/O模型

3. I/O复用模型(最常用)

4.信号驱动I/O模型

5. 异步IO模型(效率最高)

select

socket编程(九)

select读、写、异常事件发生条件

socket编程(十)

close与shutdown的区别:


socket编程(七)

TCP 11种状态

图上只包含10种状态,还有一种特殊的状态:CLOSING(产生原因是双方同时关闭)

(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态(如下图)。

 TIME_WAIT状态(由发起colse的一端产生)

 

 

 //TCP状态转换图【11种状态】 是 针对“一个TCP连接【一个socket连接】”来说的;

  //客户端: CLOSED ->SYN_SENT->ESTABLISHED【连接建立,可以进行数据收发】

  //服务端: CLOSED ->LISTEN->【客户端来握手】SYN_RCVD->ESTABLISHED【连接建立,可以进行数据收发】

  //谁主动close连接,谁就会给对方发送一个FIN标志置位的一个数据包给对方;【服务器端发送FIN包给客户端】

  //服务器主动关闭连接:ESTABLISHED->FIN_WAIT1->FIN_WAIT2->TIME_WAIT

  //客户端被动关闭:ESTABLISHED->CLOSE_WAIT->LAST_ACK

连接建立三次握手、连接终止四次握手

TIME_WAIT与SO_REUSEADDR

TIME_WAIT 状态也称为 2MSL 等待状态。每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime)。

四次挥手主动关闭的一方就会产生这个状态)

 //setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前 

SO_REUSEADDR的能力:

  //(1)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;

                 //【即便TIME_WAIT状态存在,服务器bind()也能成功】

  //(2)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;

  //(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;

  //(4)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持
 

RST标志

 //对于每一个TCP连接,操作系统是要开辟出来一个收缓冲区,和一个发送缓冲区 来处理数据的收和发;

      //当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;

      //FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;

 

      //反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;

      //如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;

      //RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;

SIGPIPE

如果对方socket已关闭,对等方再发写数据,则会产生SIGPIPE信号

往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可

signal(SIGPIPE, SIG_IGN);

 

 Client端测试代码:(请注意观察 27-39 , 44,53 行代码)

//client端完整代码实现及解析
#include "commen.h"

//return a socket that have connected to server.
int mkATCPClient(int serverPort, string serverIPAddr)
{
  //first. create a socket
  int sockfd = socket(AF_INET,SOCK_STREAM,0);
  if (sockfd == -1)
  {
    err_exit("socket error");
  }

  //second. connect to a server
  struct sockaddr_in serverAddr;
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_port = htons(serverPort);
  serverAddr.sin_addr.s_addr = inet_addr(serverIPAddr.c_str());
  if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1)
  {
    err_exit("connect error");
  }

  return sockfd;
}

//捕获SIGPIPE信号的测试代码
void onSignalCatch(int signalNumber)
{
  switch(signalNumber)
  {
  case SIGPIPE:
    cout << "receive SIGPIPE = " << SIGPIPE << endl;
     _exit(0);
  default:
    cout << "UnKnow signal" << endl;
    break;
  }
}

int main()
{
  //安装捕获SIGPIPE信号的处理服务
  signal(SIGPIPE,onSignalCatch);

  int serverSocket = mkATCPClient(8002,"127.0.0.1");

  char sendBuf[BUFSIZ];
  //从键盘输入数据
  while (fgets(sendBuf,sizeof(sendBuf),stdin) != NULL)
  {
    //向server发送数据(注意在server端关闭时也要发送数据,注意观察现象)
    if (writen(serverSocket,sendBuf,strlen(sendBuf)) == -1)
    {
      err_exit("write socket error");
    }
  }
  close(serverSocket);

  return 0;
}

说明:在server端关闭(或者是处理该客户端的子进程关闭)后,仍继续往socket中写数据,则会观察到如下现象:

 

socket编程(八)

五种I/O模型

五种I/O模型分别是阻塞式I/O,非阻塞式I/O,信号驱动,I/O复用(这四种是同步I/O),异步I/O。

1. 阻塞I/O模型

 最常用的模型就是阻塞I/O模型,默认情况下,所有文件的操作都是阻塞的。应用进程调用recvfrom获取数据,系统调用直到数据包被复制到用户进程的缓冲区或发生错误时返回。从调用recvfrom到系统返回的整段时间内,应用进程都是被阻塞的,因此被称为阻塞I/O模型。

2. 非阻塞I/O模型

用户进程只有在第二个阶段被阻塞了,而第一个阶段没有阻塞,但是在第一个阶段中,用户进程需要盲等,不停的去轮询内核,看数据是否准备好了,因此该模型是比较消耗CPU的。

3. I/O复用模型(最常用)

 Linux系统提供select/poll,进程将fd传给select/poll系统调用,阻塞在select操作上。select/poll顺序扫描fd是否就绪,如果数据变为可读状态,应用进程调用recvfrom获取数据。

4.信号驱动I/O模型

需要开启套接口信号驱动I/O功能,应用进程通过系统调用sigaction后立即返回继续工作(非阻塞)。当数据准备就绪后,系统为该应用进程生产一个SIGIO信号,通过信号回调通知应用程序调用recvfrom获取数据。

5. 异步IO模型(效率最高)

应用进程告知内核执行某个动作,并让内核在操作完成之后(包括数据从内核空间复制到用户空间)通知该进程。

异步与信号驱动的区别:

信号驱动:内核通知进程,数据包已经准备好了,可以调用recvfrom来获取数据了

异步:内核通知进程,数据准备好了,并且也已经从内核拷贝到了用户空间

select

select管理者

用select管理多个IO

一旦其中的一个IO或者多个IO检测到我们所感兴趣的事件  slect函数返回 返回值为检测到事件的个数

函数原形:

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

函数功能:

实现多路复用,也就是说可以同时监听多个文件描述符;

所属头文件:

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

返回值

超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

参数说明:

maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间


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

跟select一起使用的函数的作用

void FD_ZERO(fd_set *set);//清空一个文件描述符的集合
void FD_SET(int fd, fd_set *set);//将一个文件描述符添加到一个指定的文件描述符集合中
void FD_CLR(int fd, fd_set *set);//将一个指定的文件描述符从集合中清除;
int  FD_ISSET(int fd, fd_set *set);//检查集合中指定的文件描述符是否可以读写

例子

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, cahr **argv)
 {
    struct timeval timeout;
    int ret = 0;
    fd_set rfds;
 
    FD_ZERO(&rfds);//清空描述符集合 
    FD_SET(0, &rfds);//将标准输入(stdin)添加到集合中
    FD_SET(sockfd, &rfds);//将我们的套接字描述符添加到集合中
    /*设置超时时间*/
    timeout.tv_sec = 1;   
    timeout.tv_usec = 0;

    /*监听套接字是否为可读*/
    ret = select(sockfd+1, &rfds, NULL, NULL, &timeout);
    if(ret == -1) {//select 连接失败
        perror("select failure\n");
        return 1;
    }   
    else if(ret == 0) {//超时(timeout)
        return 1;
    }   

    if(FD_ISSET(pesockfd, &rfds)) {//如果可读
                    
            //recv or recvfrom
            ..................
    }
    
   return 0; 
  }

 

socket编程(九)

select读、写、异常事件发生条件

1.可读:

                套接口缓冲区有数据可读。

                连接的读一半关闭,即接收到FIN段,读操作将返回0。

                如果是监听套接口,已完成连接队列不为空时。

                套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。

2.可写:

                套接口发送缓冲区有空间可容纳数据。

                连接的写一半关闭。即收到RST段之后,再次调用write操作。

                套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。

3.异常:

                套接口存在带外数据。

socket编程(十)

close与shutdown的区别:

close()函数:

close一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,然而TCP将尝试发送已经排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用技术会相应的减一,当引用计数仍大于0时,这个close调用就不会引发TCP的四路握手断连过程。

shutdown()函数:

int shutdown(int sockfd,int how)

该函数行为依赖how的值。

 1.SHUT_RD:值为0,关闭连接的读这一半,套接字中不再有数据接收,且套接字接收缓冲区中的现有数据全都被丢弃,该套接字描述符不能再被进程调用,对端发送的数据会被确认,然后丢弃。

 2.SHUT_WR:值为1,关闭连接的写这一半。这称为半关闭,当前在套接字发送缓冲区数据被发送,然后连接终止序列。不论套接字描述符引用技术是否等于0,写半部都会被关闭。

 3.SHUT_RDWR:值为2,连接的读和写都关闭。相当于先调用SHUT_RD,再调用SHUT_WR。

close与shutdown的区别:

①:close函数函数会关闭套接字,如果由其他进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。

②:shutdown会切断进程共享的套接字的所有连接,不管引用计数是否为0,由第二个参数选择断连的方式
 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值