Socket通讯及其TCP/IP原语
Socket通讯
Socket提供了一个通信端口,应用程序在网络上传输/接收的信息都通过Socket接口来实现的。在应用开发中可以像使用文件句柄一样来对Socket句柄进行读写操作。Windows Socket与协议无关并向下兼容,可以使用任何底层传输协议提供的通信能力,来为上层应用程序完成网络数据通讯,而不关心底层网络链路的通讯情况,实现了底层网络通讯对应用程序的透明。在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式。
在开始使用socket套接字编程之前,首先必须建立一下概念:
端口
传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某个标识符。为此,TCP/IP协议提出了协议端口(protocol port 简称端口)的概念,用于标识通信的进程。应用程序(即进程)通过系统调用与某端口建立链接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问。
注意:当调用bind()的时候,不要把端口数设置的过小,小于1024的所有端口都是保留下来作为系统使用端口的,没有root权利无法使用,可以使用1024以上的任何端口,一直到65535。最后注意有关bind()的是:有时候并不一定要调用bind()来建立网络连接,比如你只是想连接到一个远程主机上面进行通讯,并不在乎你究竟是用的自己机器上的哪个端口进行通讯(比如Telnet),那么你可以简单的直接调用connect()函数,connect()将自动寻找出本地机器上的一个未使用的端口,然后调用bind()来将其socket 绑定到那个端口上。
地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接,因此需要三级寻址:
1.某一个主机可与多个网络相连,必须指定一特定网络地址。
2.网络上每一台主机应有其唯一的地址;
3.每一个主机上的每一个进程应有在该主机上的唯一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。
重复服务和并发服务
在客户/服务器模式中,有两种类型的服务:重复服务和并发服务。accept()调用为实现并发服务提供了极大方便,因为它要返回一个新的套接字号,其典型结构为:
int initsockid, newsockid;
if ((initsockid = socket(….)) < 0)
error(“can’t create socket”);
if (bind(initsockid,….) < 0)
error(“bind error”);
if (listen(initsockid , 5) < 0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, …) /* 阻塞 /
if (newsockid < 0)
error(“accept error”);
if (fork() == 0){ / 子进程 /
closesocket(initsockid);
do(newsockid);
/ 处理请求 /
exit(0);
}
closesocket(newsockid); /
父进程 */
}
这段程序执行的结果是newsockid与客户的套接字建立相关,子进程启动后,关闭继承下来的主服务器的initsockid,并利用新的newsockid与客户通信。主服务器的initsockid可继续等待新的客户连接请求。由于在Unix等抢先多任务系统中,在系统调度下,多个进程可以同时进行。因此,使用并发服务器可以使服务器进程在同一时间可以有多个子进程和不同的客户程序连接、通信。在客户程序看来,服务器可以同时并发地处理多个客户的请求,这就是并发服务器名称的来由
面向连接服务器也可以是重复服务器,其结构如下:
int initsockid, newsockid;
if ((initsockid = socket(….))<0)
error(“can’t create socket”);
if (bind(initsockid,….)<0)
error(“bind error”);
if (listen(initsockid,5)<0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, …) /* 阻塞 /
if (newsockid < 0)
error(“accept error”);
do(newsockid); / 处理请求 */
closesocket(newsockid);
}
重复服务器在一个时间只能和一个客户程序建立连接,它对多个客户程序的处理是采用循环的方式重复进行,因此叫重复服务器。并发服务器和重复服务器各有利弊:并发服务器可以改善客户程序的响应速度,但它增加了系统调度的开销;重复服务器正好与其相反,因此用户在决定是使用并发服务器还是重复服务器时,要根据应用的实际情况来定。
网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。
下面给出套接字字节转换程序的列表:
htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号
短型进行操作4 bytes)
htonl()——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符
号长型进行操作8 bytes)
ntohs()——“Network to Host Short “ 网络字节顺序转换为主机字节顺序(对无符
号短型进行操作4 bytes)
ntohl()——“Network to Host Long “ 网络字节顺序转换为主机字节顺序(对无符
号长型进行操作8 bytes)
在把数据发送到Internet 之前,一定要把它的字节顺序从主机字节顺序转换到网络字节顺序!
注:在struct sockaddr_in 中的sin_addr 和sin_port 他们的字节顺序都是网络字节顺序,而
sin_family 却不是网络字节顺序的。为什么呢?
这个是因为sin_addr 和sin_port 是从IP 和UDP 协议层取出来的数据,而在IP 和UDP
协议层,是直接和网络相关的,所以,它们必须使用网络字节顺序。然而, sin_family 域
只是内核用来判断struct sockaddr_in 是存储的什么类型的数据,并且 sin_family 永远也
不会被发送到网络上,所以可以使用主机字节顺序来存储。
阻塞和非阻塞
阻塞:一般的I/O操作可以在新建的流中运用,在服务器回应前它等待客户端发送一个空白的行,当会话结束时,服务器关闭流和客户端socket。如果在队列中没有请示将会出现什么情况呢?那个方法将会等待一个的到来,这个行为叫做阻塞。accept()方法会阻塞服务器线程知道一个呼叫到来,当5个连接处理完闭之后,服务器退出,任何的在队列中的呼叫将会被取消。
非阻塞:非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲中数据,不管是否读到数据都立即返回,而不会一直挂载此函数调用上。在实际window网络通信软件开发中,异步非阻塞套接字是用的最多的,平常所说的C/S(客户端/服务器)结构的软件就是异步非阻塞模式的。
socket工作过程如下:
服务器 | 客户端 | |
---|---|---|
Socket() | socket() | |
Bind() | ||
Linsten() | ||
等待连接请求 | <—–请示连接 | connect() |
accept() | ||
Recv()&Senc() | <—–数据交互——> | Recv()&Send() |
closesocket() | Closesocket() | |
服务器首先启动,通过调用socket()建立一个套接字,然后调用bind()将该套接字和本地网络地址联系在一起,再调用listen()使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用accept()来接收连接。客户在建立套接字后就可调用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。最后,待数据传送结束后,双方调用close()关闭套接字。 | ||
## TCP/IP连接原语 | ||
1. 传输层协议 | ||
从通信和信息处理的角度看,传输层向它上面的应用层提供通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最低层,为应用进程之间提供端到端的逻辑通信。传输层还要对收到的报文进行差错检测。传输层需要有两种不同的运输协议,即面向连接的TCP和无连接的UDP。 |
TCP/IP结构体系中的传输层协议
应用层 | |
---|---|
传输层 | <—–(UDP、TCP) |
IP | |
与各种网络接口 |
传输层协议和网络层协议的主要区别
| 应用进程<—–>主机<——->因特网<——->主机<——->应用进程 |
|——-|——|
| IP协议的作用范围 |
| ——(提供主机之间的逻辑通信)—— |
| TCP和UDP协议的作用范围 |
| ——(提供进程之间的逻辑通信)—– |
TCP头和IP头对应关系
| TCP报文段=|TCP首部|+|TCP数据部分|
|IP首部|IP数据部分|
下图为TCP首部
| TCP首部 |
| 源端口 | 目的端口 |
| 序号 |
| 确认号 |
| 数据偏移 |保留|URG|ACK|PSH|RST|SYN|FIN|窗口|
| 检验和 | 紧急指针 |
| 选项(长度可变)|填充 |
在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN, FIN, ACK, PSH, RST, URG.其中, SYN表示建立连接;FIN表示关闭连接;ACK表示响应;PSH表示有DATA数据传输;RST表示连接重置。
用三次握手建立TCP连接
|主机A| |主机B|
|主动打开||被动打开|
|连接请求|——SYN,SEQ=X——| |
||<——SYN,ACK,SEQ=Y,ACK=x+1—-|确认|
||<—–SYN,ACK,SEQ=X+1,ACK=Y+1—–||
A的TCP向B发出连接请求报文段,其首部中的同步比特SYN应置为1,并选择序号x,表明传送数据时的第一个数据字节的序号是x。B的TCP收到连接请求报文段后,如同意则发回确认。B在确认报文段中应将SYN置为1,其确认号应为x+1,同时也为自己选择序号y。A收到此报文段后,向B给出确认,其确认号应为y+1。A的TCP通知上层应用进程,连接已经建立。当运行服务器进程的主机B的TCP收到主机A的确认后,也通知其上层应用进程,连接已经建立。
TCP连接释放的过程
|主机A||主机B|
|——|——|
|应用进程释放连接|—–FIN,SEQ=X—–>|通知主机应用进程|
||<—–ACK,SEQ=y,ACK=X+1—–|确认|
|确认|——-SEQ=Y+1——->||
TCP状态转移
LISTEN: 侦听来自远方的TCP端口的连接请求
SYN-SET: 在发送连接请求后等待匹配的连接请求
SYN-RECEIVED: 再收到和发送一个连接请求后等待对方对连接请求的确认
ESTABLISHED: 代表一个打开的连接
FIN-WAIT-1: 等待远程TCP连接中断请求,或先前的连接中断请求的确认。
FIN-WAIT-2:从远程TCP等待连接中断请求
CLOSE-WAIT:等待从本地用户发来的连接中断请求
CLOSING:等待远程TCP对连接中断的确认
LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认
CLOSED:没有任何连接状态
建立连接过程
- 所有连接开始于CLOSED状态。
- 服务器先执行一个被动TCP打开操作,使TCP转到LISTEN状态。
- 客户执行一个主动TCP打开操作,发送SYN,转到SYN_SENT状态。
- 当服务器收到SYN后,发送SYN+ACK, 转到SYN_RCVD。
- 当客户收到SYN+ACK后,向服务器发送ACK,转到ESTABLISHED状态。
- 当服务器收到ACK后,转到ESTABLISHED状态。
以上完成三次握手。
注:1) 如果客户发送到服务器的ACK(第三次握手)丢失,因为客户已经处于ESTABLISHED状态,连接仍能正常工作,所以客户应用进程可以向服务器开始发送数据。
2) 超时重传机制没有在状态转移图中显示。大多数状态向另一方发送数据的同时也调用超时机制。如果没有出现期望的响应,则重发该数据。若数次重发仍没有得到期望的响应,TCP则放弃重传并回到CLOSED状态。终止连接过程
终止连接过程:
各方的应用进程必须独立地关闭自己一方的连接。若一方关闭,该方不再发送数据,但仍能接收发来的数据。任何一方从ESTABLISHED状态转到CLOSED状态都有三种转换组合:
1) 这一方先关闭:
ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
2) 另一方先关闭:
ESTABLISHED -> CLOSE_WAIT ->LIST_ACK -> CLOSED
3) 双方同时关闭:
ESTABLISHED -> FIN_WAIT_1 -> CLOSING -> TIME_WAIT -> CLOSED
TCP的重传机制
TCP每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段。
上面的概念容易理解,我们这里说一下快重传。快重传算法规定,发送端只要一连收到三个重复的ACK即可断定有分组丢失了,就应立即重传丢失的报文段而不必继续等待为该报文段设置的重传计时器的超时。不难看出,快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失的报文段。
快重传举例
|主机A—–主机B||
|—M1,M2—->|A发送M1和M2|
|<—-ACK2,ACK3—-|B确认M1和M2|
|—–M3—–>丢失|A发送M3但丢失了|
|—-M4—–|A发送M4|
|<—-ACK3—-|B只能再次确认M2(因为M3没有收到)|
|—-M5——->|A发送M5|
|<—-ACK3—-|B发送第二个重复确认ACK3|
|—–M6——>|A发送M6|
|<—–ACK3——|B发送第三个重复确认ACK3|
|—-M3—–>|A收到了三个重复的确认ACK3,就立即重传M3,而不必等待超时重传|