物联网之Linux网络编程二

本篇主讲内容

1.IO模型和多路复用模型

2.网络分析测试工具、封包、IP和TCP头

3.TCP握手过程

4.网络信息检索、网络属性设置、超时检查

IO模型

在UNIX/Linux下主要有4种I/O 模型:(详细讲解请往下看)

阻塞I/O:最常用

非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询

I/O 多路复用:允许同时对多个I/O进行控制

信号驱动I/O:一种异步通信模型

阻塞I/O 模式

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。

前面学习的很多读写函数在调用过程中会发生阻塞。

    ---读操作中的read、recv、recvfrom(sendto为非阻塞)

    ---写操作中的write、send

    ---其他操作:accept、connect

读阻塞

以read函数为例:

    进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发生阻塞。

    它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。

    经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据。

    如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

写阻塞

在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。

这时,写操作不进行任何拷贝工作,将发生阻塞。

一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。

UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。

非阻塞模式I/O

当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

这种模式使用中不普遍。

非阻塞模式的实现---fcntl()函数

当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。

可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。

代码实现:

int fcntl(int fd, int cmd, long arg);       

int flag;       

flag = fcntl(sockfd, F_GETFL, 0);       

flag |= O_NONBLOCK;       

fcntl(sockfd, F_SETFL, flag);

另外,也可以使用ioctl()函数实现,实现代码:

int b_on =1;    

ioctl(sock_fd, FIONBIO, &b_on);

多路复用I/O

应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;

若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;

比较好的方法是使用I/O多路复用。其基本思想是:

    ---先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

    ---函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

实现多路复用---select()/poll()

select()函数:

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

int select(int n, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);

参数maxfd:所有监控的文件描述符中最大的那一个加1

参数read_fds:所有要读的文件文件描述符的集合

参数write_fds:所有要写文件文件描述符的集合

参数except_fds:其他要向我们通知的文件描述符的集合

参数timeout:超时设置.

    ---Null:一直阻塞,直到有文件描述符就绪或出错

    ---时间值为0:仅仅检测文件描述符集的状态,然后立即返回

    ---时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

在我们调用select时进程会一直阻塞(timeout设置为NULL或非0)直到以下的一种情况发生.

    ---有文件可以读.

    ---有文件可以写.

    ---超时所设置的时间到.

为了设置文件描述符我们要使用几个宏:

    ---FD_SET       将fd加入到fdset 

    ---FD_CLR      将fd从fdset里面清除

    ---FD_ZERO   从fdset中清除所有的文件描述符

    ---FD_ISSET   判断fd是否在fdset集合中

宏的形式:  

    void FD_SET(int fd,fd_set *fdset) 

    void FD_CLR(int fd,fd_set *fdset)

    void FD_ZERO(fd_set *fdset)

    int FD_ISSET(int fd,fd_set *fdset)

poll()函数:

#include <sys/poll.h>

int poll(struct pollfd *ufds, unsigned int nfds, int timeout);

TCP多路复用流程图

关键点:

1.select( )函数里面的各个文件描述符fd_set集合的参数在select( )前后发生了变化:  

    ---前:表示关心的文件描述符集合  

    ---后:有数据的集合(如不是在超时情况下)

2.那么究竟是谁动了fd_set集合的奶酪?

    ---答曰:kernel

在client/server模型中,TCP的三次握手和四次握手过程

(1)三次握手的详述
首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。
最初两端的TCP进程都处于CLOSED关闭状态,A主动打开连接,而B被动打开连接。(A、B关闭状态CLOSED——B收听状态LISTEN——A同步已发送状态SYN-SENT——B同步收到状态SYN-RCVD——A、B连接已建立状态ESTABLISHED)
B的TCP服务器进程先创建传输控制块TCB,准备接受客户进程的连接请求。然后服务器进程就处于LISTEN(收听)状态,等待客户的连接请求。若有,则作出响应。
1)第一次握手:A的TCP客户进程也是首先创建传输控制块TCB,然后向B发出连接请求报文段,(首部的同步位SYN=1,初始序号seq=x),(SYN=1的报文段不能携带数据)但要消耗掉一个序号,此时TCP客户进程进入SYN-SENT(同步已发送)状态。
2)第二次握手:B收到连接请求报文段后,如同意建立连接,则向A发送确认,在确认报文段中(SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y),测试TCP服务器进程进入SYN-RCVD(同步收到)状态;
3)第三次握手:TCP客户进程收到B的确认后,要向B给出确认报文段(ACK=1,确认号ack=y+1,序号seq=x+1)(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。TCP连接已经建立,A进入ESTABLISHED(已建立连接)。
当B收到A的确认后,也进入ESTABLISHED状态。

(1)四次挥手的详述
假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
 数据传输结束后,通信的双方都可释放连接,A和B都处于ESTABLISHED状态。(A、B连接建立状态ESTABLISHED——A终止等待1状态FIN-WAIT-1——B关闭等待状态CLOSE-WAIT——A终止等待2状态FIN-WAIT-2——B最后确认状态LAST-ACK——A时间等待状态TIME-WAIT——B、A关闭状态CLOSED)
1)A的应用进程先向其TCP发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
2)B收到连接释放报文段后即发出确认报文段,(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
3)A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
4)B没有要向A发出的数据,B发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
5)A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSED状态。

网络信息检索函数

gethostname() 获得主机名

getpeername() 获得与套接口相连的远程协议地址

getsockname() 获得本地套接口协议地址

gethostbyname() 根据主机名取得主机信息

    ->endhostent()

gethostbyaddr() 根据主机地址取得主机信息

getprotobyname() 根据协议名取得主机协议信息

getprotobynumber() 根据协议号取得主机协议信息

getservbyname() 根据服务名取得相关服务信息

getservbyport() 根据端口号取得相关服务信息

网络属性设置---getsockopt和setsockopt

#include <sys/socket.h>

int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen);

int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);

返回值:成功返回0   失败返回-1并设置errno

level指定控制套接字的层次.可以取三种值:

    1)SOL_SOCKET:通用套接字选项.

    2)IPPROTO_IP:IP选项.

    3)IPPROTO_TCP:TCP选项.

optname指定控制的方式(选项的名称),我们下面详细解释

optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换

optlen设置套接字选项的数据类型的长度:sizeof(数据类型)

选项名称                  说明                 数据类型 
                                                 SOL_SOCKET 
SO_BROADCAST      允许发送广播数据            int 
SO_DEBUG          允许调试                int
SO_DONTROUTE        不查找路由               int
SO_ERROR          获得套接字错误             int
SO_KEEPALIVE         保持连接                int
SO_LINGER            延迟关闭连接              struct linger  
SO_OOBINLINE         带外数据放入正常数据流         int
SO_RCVBUF          接收缓冲区大小             int
SO_SNDBUF          发送缓冲区大小             int
SO_RCVLOWAT       接收缓冲区下限             int
SO_SNDLOWAT       发送缓冲区下限             int
SO_RCVTIMEO        接收超时                struct timeval
SO_SNDTIMEO        发送超时                struct timeval
SO_REUSERADDR       允许重用本地地址和端口         int
SO_TYPE            获得套接字类型             int
SO_BSDCOMPAT       与BSD系统兼容              int 
                 IPPROTO_IP  
IP_HDRINCL          在数据包中包含IP首部           int 
IP_OPTINOS          IP首部选项               int
IP_TOS            服务类型  
IP_TTL             生存时间                int
                 IPPRO_TCP  
TCP_MAXSEG        TCP最大数据段的大小           int 
TCP_NODELAY      不使用Nagle算法               int 
    允许绑定地址快速重用示例:

    if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

网络超时

在网络通信中,很多操作会使得进程阻塞

TCP套接字中的recv/accept/connect

UDP套接字中的recvfrom

超时检测的必要性

    ---避免进程在没有数据时无限制地阻塞

    ---当设定的时间到时,进程从原操作返回继续运行

网络超时检测(一)

设置socket的属性 SO_RCVTIMEO

参考代码如下:

struct timeval  tv;

tv.tv_sec = 5;   //  设置5秒时间
tv.tv_usec = 0;

setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,  &tv, sizeof(tv));   //  设置接收超时
recv() / recvfrom()    //   从socket读取数据

网络超时检测(二)

用select检测socket是否’ready’

    参考代码如下:

    struct fd_set rdfs;
    struct timeval  tv = {5 , 0};   // 设置5秒时间

    FD_ZERO(&rdfs);
    FD_SET(sockfd, &rdfs);

    if (select(sockfd+1, &rdfs, NULL, NULL, &tv) > 0)   // socket就绪
    {
          recv() /  recvfrom()    //  从socket读取数据
    }

网络超时检测(三)

参考代码如下:

void  handler(int signo)     {   return;  }

struct sigaction  act;
sigaction(SIGALRM, NULL, &act);
act.sa_handler = handler;
act.sa_flags &= ~SA_RESTART;
sigaction(SIGALRM, &act, NULL);
alarm(5);
if (recv(,,,) < 0) ……

猜你喜欢

转载自blog.csdn.net/weixin_39148042/article/details/81151057