本篇主讲内容:
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) ……