stevens那本socket的第一部分的简要翻译
以前被流放到乡下,无聊时翻的,昨天从盘上翻出来,觉得从来没在网上贴过,埋没了可惜,就灌到CSDN来了,高手不要笑话我,转贴也不用注明了
第一部分:TCP/IP
====================
Client-Server的概念:
====================
譬如WWW Server和浏览器,FTP Server和各式各样的FTP工具,等等。
一般意义上的Server提供一种特定的服务,运行在后台,并没有直接
和用户交互的界面、命令行等等,它响应Client的服务请求,并把Client
需要的数据返回给Client,当然,其中可能要经过数据库操作或计算等等。
Client是客户端程序,直接与用户或操作员发生交互,与远程的服务
器上的或本地的后台Server通信,完成用户的请求。一个Client在任何时
刻只能和一个确定的Server通信,而Server,可以同时处理多个Client。
Client-Server是编写网络程序的一般模式(在某些情况下可能会有
更复杂的结构,譬如一个应用程序既做Server又做Client)。
应用程序的通信必须通过网络协议,本文中只讨论TCP/IP协议族。
本文不按照ISO的OSI七层网络分层结构,而是把OSI结构中最上面的
三层(应用层、表示层、会话层)统称应用层,把OSI结构中的最下面两
层(链路层、物理层)统称链路层。这样,我们的分层结构就是:
┏━━━━━┯━━━━━━━━━━━━━━━━━┓
┃ 应用层 │与用户交互的应用程序和服务 ┃
┠─────┼─────────────────┨
┃ 传输层 │TCP & UDP ┃
┠─────┼─────────────────┨
┃ 网络层 │IPv4 & IPv6 ┃
┠─────┼─────────────────┨
┃ 链路层 │网卡驱动程序、网卡本身、传输介质 ┃
┗━━━━━┷━━━━━━━━━━━━━━━━━┛
Client和Server不一定要在同一网段,可以是通过广域网连接的。当
然中间必然要经过路由器。
============================
一个Daytime的TCP Client例子:
============================
1 #include "unp.h"
2 int
3 main( int argc, char **argv[])
4 {
5 int sockfd,n;
6 char recvline[MAXLINE + 1];
7 struct sockaddr_in servaddr;
8 if ( argc != 2 )
9 err_quit("usage: %s <IP address>", argv[0] );
10 if( (sockfd = socket( AF_INET, SOCK_STREAM, 0) < 0 )
11 err_sys("socket error");
12 bzero( &servaddr, sizeof(servaddr) );
13 servaddr.sin_family = AF_INET;
14 servaddr.sin_port = htons(13);
15 if ( inet_pton( AF_INET, argv[1], &servaddr.sin_addr) <= 0)
16 err_quit("inet_pton error for %s", argv[1]);
17 if ( connect( sockfd, (SA *)&servaddr, sizeof(servaddr) ) <= 0 )
18 err_sys("connet error");
19 while( (n = read( sockfd, recvline, MAXLINE)) > 0 ){
20 recvline[n] = 0;
21 if ( fputs( recvline, stdout) == EOF )
22 err_sys("fputs error");
23 }
24 if ( n < 0 )
25 err_sys("read error");
26 exit(0);
27 }
这个程序运行后的结果大致如下:
# a.out 206.62.226.35
Fri Jan 12 14:27:52 1999
这个27行的程序中有许多细节问题,我们先大概看一下:
1 :unp.h是我们自己的头文件,里面有一些宏定义,和一些include
语句。
2 - 3 :命令行参数
10 - 11 :创建一个TCP socket,所谓TCP socket,其实就是一个Internet
(AF_INET)的stream(SOCK_STREAM) socket,注意Internet的大写
的I,这样的写法指互连的网络,而不一定要是因特网;stream即
字节流的意思。与之对应的是SOCK_DATAGRAM,数据报的socket,
即:UDP。
12 - 16 :填写socket的地址结构,填入server端的IP地址和要连接的端口
号(port_number)。我们先是通过bzero把整个结构的内容置为0,
把地址族address family填为:AF_INET,地址族是内核需要的一
个参数,尽管我们的程序都是用AF_INET,但实际上内核可以处理
多种地址结构,一个典型的例子是:AF_UNIX,只限于本地进程间
通信的socket地址结构。端口号赋为我们需要的服务的端口号,
在本例中是Daytime的端口:13,IP地址赋为命令行的参数值,由
于在这个地址结构中这些值都有特定的格式,所以我们用库函数:
htons :host to network short 和
inet_pton :presentation to numeric
来转换格式。
注意:对于某些早期的IPv4,不支持inet_pton,改用:inet_addr。
17 - 18 :connect调用实现和servaddr结构中指定的IP & port的连接,注意
connect的第三个参数,这是servaddr结构的大小,这个参数也是
内核为了处理多种地址结构所必需的。
#define SA struct sockaddr_in
这样的写法不使程序行过长。
19 - 25 :读取server的返回,并输出到屏幕(stdout)。
这里要注意,由于TCP是字节流协议,server返回的字符串可能是在
一个TCP段(TCP segment)内,也有可能是在很多个TCP段内,所以
我们在从TCP socket中读取数据的时候总是把read放在循环内,直
到read返回0(对方关闭了socket)或者返回小于0的数(error)。
在本例子中,是由于server关闭了它的socket,使得我们程序中的
read返回0,这种形式也存在于HTTP中;但还有其他的协议,譬如FTP
和SMTP,是在每个记录尾部标记回车换行,还有一种情况,是在每个
记录头部先声明此记录的长度,SUN RPC和DNS就是这样的。
最后要说明的是:TCP本身不提供记录边界的服务,应用程序如果需
要知道一个记录是否结束,它必须自己想办法,譬如象上面这三种。
26 :程序结束。UNIX会关闭所有没有关闭的文件句柄,包括socket。
东西太多了,慢慢来,不急。
==========
协议无关性
==========
我们上面这个程序是建立在IPv4的基础上的,如果我们要它在IPv6下工作,我
们要做如下改动:
7 struct sockaddr_in6 servaddr;
10 if( (sockfd = socket( AF_INET6, SOCK_STREAM, 0) < 0 )
13 servaddr.sin6_family = AF_INET6;
14 servaddr.sin6_port = htons(13);
15 if ( inet_pton( AF_INET6, argv[1], &servaddr.sin6_addr) <= 0)
也就是所有用到AF_INET或者servaddr.sin_addr的地方。以后会有一个叫:
getaddrinfo的调用,可以让我们编写出与协议无关的程序。
人们总是习惯于说:169.xici.net.cn的HTTP服务,而不是说:10.74.32.74的
80端口。所以我们必须要能把域名和服务转换成系统调用能接受的形式,有这样的
函数:gethostbyname和getservbyname。
========
出错处理
========
我们不可能每个程序都这样写:
if ( (sockfd = socket ( AF_INET, SOCK_STREAM, 0) < 0 )
err_sys("socket error");
我们这样:
int
Socket( int family, int type, int protocol)
{
int n;
if ( (n = socket( family, type, protocol )) < 0 ){
err_sys("socket error");
return n;
}
这样做的目的是使程序看起来不那么长。
每次系统调用出错时,系统调用返回-1,UNIX的全局变量errno被赋值,其值在
<sys/errno.h>中有详细的描述。
============================
一个Daytime的TCP Server例子:
============================
我们来自己写个Server吧:
1 #include "unp.h"
2 #include <time.h>
3 int
4 main( int argc, char **argv[])
5 {
6 int listenfd, connfd;
7 struct sockaddr_in servaddr;
8 char buff[MAXLINE];
9 time_t ticks;
10 listenfd = Socket( AF_INET, SOCK_STREAM, 0 );
11 bzero( &servaddr, sizeof(servaddr) );
12 servaddr.sin_family = AF_INET;
13 servaddr.sin_addr.s_addr = htons(INADDR_ANY);
14 servaddr.sin_port = htons(13);
15 Bind( listenfd, (SA *)&servaddr, sizeof(servaddr) );
16 Listen( listenfd, LISTENQ );
17 for( ; ; ) {
18 connfd = Accept( listenfd, (SA *)NULL, NULL );
19 ticks = time( NULL );
20 snprintf( buff, sizeof(buff), "%.24s", ctime(&ticks) );
21 Write( connfd, buff, strlen(buff) );
22 Close( connfd );
23 }
24 }
11 - 15 :通过填写地址结构和调用bind,把端口13和listenfd绑定;INADDR_ANY
的意思是允许listenfd接受任何地址来的连接请求,如果我们不希望这
样,我们也可以指定listenfd可以接受请求的地址。随后讨论。
16 :调用listen,listenfd进入listening状态,LISTENQ是告诉内核可以为
连接请求开多大的队列,最大值一般是5。
17 - 21 :accept是个阻塞调用,程序执行到这里,就停下,等待连接请求,当连
接请求到达,并且TCP的三段握手完成后,accept返回,返回一个新的
文件句柄:connfd,对每个不同的连接,accept返回不同的connfd,与
listenfd不同,listenfd只用于等待连接请求,而对每一个已经实现的
连接,我们是用这个连接的connfd来通信,交换数据。
snprintf是为了防止恶意攻击而采取的手段,如果用sprintf的话,大量
的服务请求很容易导致缓存溢出,与此类似有危险的函数还有:gets,
strcat,strcpy,通常用:fgets,strncat,strncpy来代替。
同上,这个程序也是与协议有关的,我们同样可以使用getaddrinfo来使得它与协
议无关。
这个server只能同时处理一个服务请求,在处理这个请求的过程中(尽管这个时间
非常短)不能接受新的请求,为了能够并发的处理client的服务请求,我们要用到UNIX
的系统调用:fork,为每个连接创建一个子进程,处理完后子进程自然死亡,还有一种
办法是事先就生成许多子进程,处理完后可以循环使用,后者一般不常用。
如果我们要让这个server长时间的运行在后台,我们一般会再改动些代码,来配合
UNIX下的superserver:inetd,这个以后再说。我们现在可以简单的用&把它放到后台。
当然,如果真要运行的话,端口不能用13,它已经被系统里正宗的Daytime server先用
了,我们必须换一个。
========
常用命令
========
netstat、ifconfig、ping,是最常用的三个检查网络状态的命令。可以查看man联
机文档获得详细用法。
大致用法是:
netstat
-i :查看网络接口,譬如:lo0、eth0
-r :查看路由表
-a :查看所有socket状态
-n :以数字形式显示
ifconfig
后面跟系统中定义的接口名,查看接口的详细资料
ping
后面跟IP地址,通过ICMP协议,检查网络的连通性;也可以通过广播地址,了解一
个网段内主机的运行情况;可以通过-s指定ICMP包的大小,来检查线路质量。
======
IP简介
======
IP层提供的是一种无连接无确认的数据报发送服务,IP层尽最大努力向目的地发送
数据报,但并不保证数据完整正确的到达,可靠性必须由上层协议来完成,譬如TCP,如
果使用UDP,则由应用程序自己负责。
IP层的核心内容:数据的分段、重装,网间中继和路由选择。
IPv4的结构:(IPv6就算了、、、里面好多东西还没定型)
0 3 4 7 8 15 16 31
┏━━━━┯━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━━┓ ──
┃ version│ header │ type of service│ total length ( in bytes ) ┃ ↑
┃ (4) │ length │ │ ┃ │
┠────┴────┴────────┼─┬─┬─┬──────────┨ │
┃ identification │0 │DF│MF│ fragment offset ┃
┠─────────┬────────┼─┴─┴─┴──────────┨ 20
┃time to live (TTL)│ protocol │ header checksum ┃ bytes
┠─────────┴────────┴────────────────┨
┃ 32-bit source IPv4 address ┃ │
┠───────────────────────────────────┨ │
┃ 32-bit destination IPv4 address ┃ ↓
┠───────────────────────────────────┨ ──
┋ options ( if any ) ┋
┠───────────────────────────────────┨
┋ ┋
┋ data ┋
┋ ┋
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
具体含义如下:
version :4位版本号,值为4(IPv4)
header length :整个IP头的长度,包括任选项(options),单位是四字节(32-bits),
由于这个域是4位,最大值为15,所以,IP头的最大长度为:60字节,
这60字节包括了20个字节的固定头部,40字节的任选项。
type of service :8位服务类型字段,由三位先导位(无定义)和四个服务类型位和
一个保留位(必须为0)组成,我们可以通过socket的IP_TOS选项
来设定服务类型:要求尽量大的传输量还是尽量小的延迟,或者是
尽量大的可靠性、尽量小的开销。
total length :16位,整个IP包的长度,包括了头部,我们可以算出,实际的载荷长
度是本字段的值减去四倍的header length的值。这个字段的一个不经
常提及的作用是:通常链路层都有一个最小数据帧长度,譬如802.3网
有个64字节的限制(计算机网络213-214),有可能一个合法的IP包不
能满足这个要求,本地链路层就会添加填充字段,以满足物理需要,远
端链路层照样会把这些填充字段交给它的IP层,对方IP层就要使用这个
长度值来判断有效数据的长度。
identification :16位,对于每个IP包都赋予不同的值,用于分段、重装。
DF & MF :do not fragment,和 more fragment的意思,这两个位和后面的13位的frag-
ment offset都用于分段和重装。
time to live : 由发送方赋值,在数据报投递的过程中,被每一个经过路由器减一,
一旦这个值被减到0,数据报被丢弃,这个字段的最大值是255,缺省
值一般是64,我们可以通过socket的IP_TTL和IP_MULTICAST_TTL选项
来设定它的值。
protocol :指明IP包中有效载荷字段内的内容使用的协议:1 (ICMPv4)、2 (IGMPv4)、
6 (TCP)、17 (UDP)、等等。
header checksum :头部(包括任选项)校验和。
address :源端和目的端IP地址。
options :一些路由选择、时戳、安全性的信息。最大40字节。可以通过getsockopt、
setsockopt的IPPROTO_IP、IP_OPTIONS来读取和设置。
关于IP地址的表示方法,我们用 :206.62.226.67/26 来表示主机206.62.226.67,它
的掩码为:255.255.255.192 (26个1),这个网段的网络地址为:206.62.226.64/26,广播
地址为:206.62.226.127。我们不再理会所谓的A、B、C类的划分。
环路地址:凡是发往地址127/8的IP包都不送往链路层,而是直接做为本地IP层的输入;
通常用这个地址来调试程序,宏定义为:INADDR_LOOPBACK 127.0.0.1(这个最常用)。
multihomed host的定义:
1、有多个接口的主机,譬如两块802.3网卡(可以在同一个网段),或者是一块802.3
网卡和一块.25网卡,或者是一块802.3网卡和一个拨号备份线路。
2、在同一接口上设置的别名,使用不同的IP地址。(这个我不大懂,没配过)
===========
ICMPv4 简介
===========
ICMP :Internet Control Message Protocol
ICMP是建立在IP之上的,用于在主机、路由器之间传递出错和控制信息,通常是由
TCP/IP软件本身使用,但也有应用程序使用ICMP,譬如:Ping、Traceroute。
0 7 8 15 16 31
┏━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━━┓
┃ type │ code │ checksum ┃
┠─────────┴────────┴────────────────┨
┋ ┋
┋ ( remainder depends on type and code ) ┋
┋ ┋
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
我们要关心的是应用程序能获得哪些ICMP消息,什么消息指明什么错误,错误原因是如
何返回到我们的程序的:
┏━━┯━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━┓
┃type│code│ Description │ Handled by or errno ┃
┠──┼──┼─────────────────┼──────────────┨
┃ 0 │ 0 │ echo reply │user process (ping) ┃
┠──┼──┼─────────────────┼──────────────┨
┃ 3 │ │ destination unreachable: │ ┃
┃ │ │ │ ┃
┃ │ 0 │ network unreachable │EHOSTUNREACH ┃
┃ │ 1 │ host unreachable │EHOSTUNREACH ┃
┃ │ 2 │ protocol unreachable │ECONNREFUSED ┃
┃ │ 3 │ port unreachable │ECONNREFUSED ┃
┃ │ 4 │ fragmentation needed │EMSGSIZE ┃
┃ │ │ but DF bit set │ ┃
┃ │ 5 │ source route failed │EHOSTUNREACH ┃
┃ │ 6 │ destination network unknown │EHOSTUNREACH ┃
┃ │ 7 │ destination host unknown │EHOSTUNREACH ┃
┃ │ 8 │ source host isolated(obsolete)│EHOSTUNREACH ┃
┃ │ 9 │ destination network │EHOSTUNREACH ┃
┃ │ │ administratively prohibited │ ┃
┃ │ 10 │ destination host │EHOSTUNREACH ┃
┃ │ │ administratively prohibited │ ┃
┃ │ 11 │ network unreachable for TOS │EHOSTUNREACH ┃
┃ │ 12 │ host unreachable for TOS │EHOSTUNREACH ┃
┃ │ 13 │ commuication administratively │ ┃
┃ │ │ prohibited │(ignored) ┃
┃ │ 14 │ host precedence violation │(ignored) ┃
┃ │ 15 │ precedence cutoff in effect │(ignored) ┃
┠──┼──┼─────────────────┼──────────────┨
┃ 4 │ 0 │ source quench │kernel for TCP ┃
┃ │ │ │ignored by UDP ┃
┠──┼──┼─────────────────┼──────────────┨
┃ │ │ redirect: │ ┃
┃ │ │ │ ┃
┃ 5 │ 0 │ redirect for networkk │kernel updates routing table┃
┃ │ 1 │ redirect for host │kernel updates routing table┃
┃ │ 2 │ redirect for TOS and network │kernel updates routing table┃
┃ │ 3 │ redirect for TOS and host │kernel updates routing table┃
┠──┼──┼─────────────────┼──────────────┨
┃ 8 │ 0 │ echo request │kernel generates reply ┃
┠──┼──┼─────────────────┼──────────────┨
┃ 9 │ 0 │ router advertisement │user process ┃
┠──┼──┼─────────────────┼──────────────┨
┃10 │ 0 │ router solicitation │user process ┃
┠──┼──┼─────────────────┼──────────────┨
┃ │ │ time exceeded: │ ┃
┃11 │ │ │ ┃
┃ │ 0 │ TTL equals 0 during transit │user process ┃
┃ │ 1 │ TTL equals 0 during reassembly│user process ┃
┠──┼──┼─────────────────┼──────────────┨
┃ │ │ parameter problem: │ ┃
┃12 │ │ │ ┃
┃ │ 0 │ IP header bad (catchall error) │ENOPROTOOPT ┃
┃ │ 1 │ required option missing │ENOPROTOOPT ┃
┠──┼──┼─────────────────┼──────────────┨
┃13 │ 0 │ timestamp request │kernel generates reply ┃
┠──┼──┼─────────────────┼──────────────┨
┃14 │ 0 │ timestamp reply │user process ┃
┠──┼──┼─────────────────┼──────────────┨
┃15 │ 0 │ information request (obsolete) │(ignored) ┃
┠──┼──┼─────────────────┼──────────────┨
┃16 │ 0 │ information reply (obsolete) │user process ┃
┠──┼──┼─────────────────┼──────────────┨
┃17 │ 0 │ address mask request │kernel generates reply ┃
┠──┼──┼─────────────────┼──────────────┨
┃18 │ 0 │ address mask reply │user process ┃
┗━━┷━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┛
所有标注:user process的地方,都可以通过raw socket(原始套接字)获得。
原始套接字:直接面向IP层的socket:SOCK_RAM。
================
TCP/IP协议族简介
================
TCP/IP协议族中最重要的两个协议就是字面上这两个:TCP和IP,此外还有
UDP、ICMP、IGMP、ARP、RARP、BPF、DLPI。
ICMP我们已经介绍过了,下面简单说一下其他几个协议:
IGMP :Internet Group Message Protocol,用于多点广播。
ARP :Address Resolution Protocol,实现IP地址和链路层物理地址的映
射关系,只在链路层是广播形式的时候需要,譬如:802.3(Ethernet)、802.4
(token bus)、802.5(token ring)、FDDI等等,在点对点连接的情况下不需要。
RARP :Reverse Address Resolution Protocol,实现从链路层物理地址
到IP地址的映射,通常用于无盘工作站的启动过程。
BPF :BSD Packet Filter
DLPI :Data Link Provider Interface
上面这两个是直接和链路层通信的协议,只有Linux提供了一种特殊的叫做
:SOCK_PACKET的socket可以利用这两个协议。
值得注意的是:在IPv6中,ICMP、IGMP、ARP、RARP都由一个叫做ICMPv6的
协议取代,使用的也不再是IPv4、而是IPv6。
===============
TCP & UDP 简介
===============
这两个协议是我们的五层结构中的传输层协议,也是我们编写socket程序的
切入点,有必要详细介绍,不过再详细也不如自己去看书。
UDP :User Datagram Protocol,用户数据报协议
提供无连接的数据报服务,即不保证数据送达目的地。它只是简单的把应用
程序的数据交给IP层,IP层把数据封装进IP包,发往目的地,没有确认!如果有
必要,应用程序必须自己负责处理确认、超时、重传、流控等。
每一个 UDP的数据报都有一个长度,我们可以把它看做是一个记录,如果数
据完整无错的到达了目的地,对方可以知道记录的长度,这点和TCP不同。
UDP 还有个好处是,在Client和Server之间没有建立连接,对于只需一个包
就完成的事务,不需要烦琐的三段握手和四个包的断开确认,Client可以在向一
个Server发送完数据后马上使用刚才这个socket向另外一个Server发送,同样,
Server的同一个socket也可以顺序接收不同的Client的数据。
TCP :Transfer Control Protocol
传输控制协议,提供面向连接的、全双工的、可靠的、顺序的、无重复的字
节流服务,TCP 协议把数据以通信双方所能接受的最大发送长度为单位把用户数
据分段发送,TCP 协议不保持数据的记录边界;TCP 内部自己有处理诸如确认、
超时、重传、流控等的机制。
面向连接:要使用TCP 通信,就必须先建立连接,然后才能交换数据,最后
还要断开连接。
可靠性: TCP向对方发送数据的时候,它会等待一个确认,如果没有收到确
认,TCP将重发一遍,继续等待确认,一定次数的重传后,TCP会放弃,并返回出
错信息。TCP中有个叫RTT:round-trip time 的算法,可以知道确认大概在什么
时候到达,RTT ,在一个LAN上可能是微秒级的,在一个WAN上可能是秒级的,而
且这个值是动态的,在某个时刻,TCP认为RTT是一秒,但是30秒后,TCP 可能就
认为RTT是两秒,全由网络当时的负荷等决定,TCP可以算出下一个确认应该等待
多长时间。通常从第一次发送到最后放弃,是4分钟到10分钟。
有序性: TCP会为它发送的每个字节标记编号。譬如,应用程序要发送一
个2K长的数据,使得TCP把它们分做两个段(所谓段,就是TCP交给IP的数据单位)
发送,那么TCP给第一个段的编号就是1-1024,第二个段的编号号就是 1025-2048,
如果数据没有按顺序到达,或者是出现了重复(由于无必要的重传引起的),对
方的TCP会根据编号排列,丢弃副本,把正确的数据交给应用程序,
流控: TCP总是告诉它的另一端,它当前能够接受多少个字节的数据,这个
值叫做活动窗口,在任一时刻,活动窗口的值总是当前接收缓冲区的空闲总和,
窗口的值是动态的,当数据到达,窗口值变小,当应用程序读取后,窗口值变大,
如果接收缓冲区已满而应用程序不读取,则窗口值为 0,这时,在接收任何数据
之前,必须等待程序把当前的数据从缓冲中取走。
全双工:程序可以在同一个TCP连接同时接收和发送数据,TCP给每个方向的
传送保留不同的活动窗口值、序列号等。(UDP也可以使用同一个 socket发送和
接收)
______________________________________________________________________
TCP连接的建立
TCP建立的时候要经过如下4个步骤:
1、Server必须准备接受一个连接请求,通过:socket、bind、listen调用,
这个过程叫做被动打开(passive open)。
2、Client通过调用connect主动打开(active open),这使得Client端的TCP
发送一个SYN段(synchronize),告诉Server,我(Client)发送数据的初始序列号,
通常这个SYN段不包含任何数据,只有IP头,TCP头,有可能有TCP选项(即将谈到)。
3、Server给Client一个确认,并且也要给Client一个SYN段,告诉Client它
的初始序列号,和它的TCP选项(和Client发过来的选项通常不一样,全双工嘛),
通常这个ACK和SYN是放在一个段里面发送的。
4、Client确认Server的SYN。
如图:
client server
connect┃ SYN J ┃
(active open)┠───────────→┨
┃ ┃accept
┃ ACK J+1, SYN K ┃
connect return┠←───────────┨
┃ ┃
┃ ACK K+1 ┃
┠───────────→┨accept return
┃ ┃
client的初始序列号是J,server的初始序列号是K,ACK J+1 的意思是:已
经收到J,下一个期望的是J+1。
这里的J、K就是包的序列号,下面那个图的M、N也是这个意思,很显然,cl-
ient的初始序列号就是J,server的初始序列号就是K。
______________________________________________________________________
TCP的选项
1、MSS:Maximum Segment Size,最大段长度,我们可以通过TCP_MAXSEG的
定义,通过getsockopt、setsockopt来读取或设置它,通常这个值与链路层的最
大帧长有关系,通过适当的设定,可以减少或是防止IP层的数据分段。
2、活动窗口大小, TCP 可以向连接的另外一端建议的最大活动窗口大小是
65535字节,因为TCP头部中这个字段的宽度是16位,最大只能到65535 ,但是在
一些速度很快的网络上(或者是时延比较大的地方:卫星),这个值不够用,于
是新的定义是把这个值左移14位,那么最大值就是65535乘以2的14次方,等于:
1073725440字节,够了。为了与早期没有这个选项的TCP兼容:TCP可以提出它建
议的窗口大小,但它只有在对方也提出这个选项的时候才对自己的窗口大小做调
整。宏定义:SO_RCVBUF。
3、Timestamp,时戳,多用于高速网,为了防止假丢包的错误,编程人员对
此无需了解。
______________________________________________________________________
TCP连接的释放要经过如下四个步骤:
1、首先调用close的一端(又叫active close),发送一个带FIN标志的TCP包,
表示数据发送结束。
2、收到FIN的一端(又叫:passive close),对FIN包做确认,在给应用程序
的接收缓冲的最后放入一个end-of-file;因为收到FIN就意味着在这个连接上不
可能再收到任何数据了。
3、一段时间后,应用程序收到end-of-file,调用close,被动关闭一端的
TCP发送FIN包。
4、主动关闭的一方收到FIN后,做确认。
如图:我们假设是client做active close。
client server
┃ FIN M ┃
close ┠───────────→┨
┃ ┃read return 0
┃ ACK M+1 ┃
┠←───────────┨
┃ ┃
┃ FIN N ┃
┠←───────────┨
┃ ┃
┃ ACK N+1 ┃
┠───────────→┨accept return
┃ ┃
通常需要四个包才能完全释放一个TCP连接,但有时候FIN M是随着数据一起
发送的,而ACK M+1和FIN N一起发送(应用程序很快就收到了end-of-file,调用
了close)。
在确认M+1后,在发送FIN N之前,有可能有数据从passive close一方发向
active close一方,这叫做:half-close,我们以后在讲shutdown调用的时候再
详细讨论。
______________________________________________________________________
TCP的状态
这个东西比较重要,因为netstat -a最后一列显示的就是这十一种状态中的
某一个。
首先我们定义CLOSED状态,就是socket的出发点,也是终点,在netstat -a
中是不可能显示出来的,因为netstat -a是不显示一个CLOSED的socket的,我们
可以把这个状态想的抽象一点,或者你也可以认为没有这个状态。
一个最普通的是ESTABLISHED状态,就是连接已经建立,正使用的状态。
由于active open的一方不一定是active close,也可能是passive close,
所以我们把open和close分开来说,首先说open过程中的状态:
active open一方:发送了SYN J后从 CLOSED 进入 SYN_SEND 状态,再收到
ACK J+1、SYN K、发送ACK K+1后,进入ESTABLISHED状态。
passive open一方:调用listen后从CLOSED进入:LISTEN状态,收到SYN J
后发送ACK J+1、SYN K,进入SYN_RCVD状态,再收到ACK K+1后进入ESTABLISHED
状态。
断开连接的时候:
active close一方:发送 FIN M后进入 FIN_WAIT_1 状态,收到ACK M+1后
进入FIN_WAIT_2状态,再收到FIN N后,发送ACK N+1,进入TIME_WAIT 状态(马
上详细讲这个TIME_WAIT)。
passive close一方:收到FIN M,发送ACK M+1,进入CLOSE_WAIT状态,当
应用程序调用了close后,发送FIN N,进入LAST_ACK状态,收到最后一个ACK
N+1后,回到CLOSED状态。
这里有一种特殊情况,就是双方同时调用close,这种情况很常见,因为TCP
不保持记录边界,当一个程序采用了先发送长度给对方的方法来标记记录的时候,
很经常出现双方同时关闭socket的情况:发送的发完就调用close,接收的先收到
长度,然后再收到长度指定的那么长的数据后,想都不想就close、、、
这样的话,有可能在发送了FIN后,还没收到ACK,先收到一个FIN,这个时候,
我们叫它:CLOSING状态,这种情况下,双方都还要发送、接收ACK,最后都进入
TIME_WAIT。
TIME_WAIT:每次active close的一方都要进入这个状态,等什么呢?
1、active close的一方也是发送最后一个ACK的一方,如果这个ACK丢失的话,
对方会再发送FIN,它就是要等这个不一定到来的FIN,如果不等待就进入CLOSED,
而ACK确实丢失,那么对方就会再发FIN,但是它永远收不到ACK了,对方的应用程
序就会收到一个error。
2、等待所有还在途中的IP包消亡。我们考虑这样一个极端的例子,它不是不
可能的:我们在206.62.226.63上ftp到198.69.10.2,本地的端口号是临时的,我
们假设它是1500,那么这个连接就是<206.62.226.63.1500 , 198.69.10.2.21>,
这个写法我们马上详细讲,在这个连接建立以后的某个时候,某个路由器或者是
某条线路出了毛病,延迟立即变大,在TIMEOUT后,包被重发,对方收到,然后这
个时候,关闭连接,假设没有TIME_WAIT状态的话,本地的临时端口号1500被释放,
然后马上就有另外一个用户与198.69.10.2的FTP端口连接,它的临时端口号也是
1500,那么,这个连接再次出现:<206.62.226.63.1500 , 198.69.10.2.21>,好,
这个时候,在线路或者是路由器出问题的时候被耽搁的包到了、、、出错、、、。
由以上两个原因我们可以看出,TIME_WAIT的时间应该能够等到那个不一定来
的FIN,还要能让所有在途中的IP包消亡:应该是2MSL,MSL:Maximum Segment
Lifetime,就是一个IP包能在网络上存活的最大时间,关于这个MSL,RFC1122 的
建议是2分钟,而BSD socket实现的时候是用的30秒。这个时间主要由IP包头部的
TTL和一些经验值决定。
如果在TIME_WAIT的过程中,ACK丢失,而对方重发的最后一个FIN又不断丢失,
直到TIME_WAIT结束,那么对方的应用程序是会收到一个 error,不过对于这种情
况,我们已经无能为力了。
TIME_WAIT时间到后,进入CLOSED状态。
______________________________________________________________________
TCP的端口号
我们要注意这么几点:
1、系统保留端口:1-1023,只有超级用户的进程才可以把这个范围里面的端
口和它的socket绑定作为server。
2、临时端口:1024-5000,这些是给本地上的client 进程用的,系统临时给
这些client进程分配这个范围内的端口号。我们要注意,如果我们的 server用了
这个范围内的端口号,那么,它不一定能在任何时候都运行成功(放在rc.d里面是
可以)。这个范围现在看来已经很小,不够用了,所以Solaris把它的范围移到了
32768-65535 ,还有一些版本的UNIX把上限5000改为50000。
3、有些client进程必须要使用保留端口号,譬如rlogin、rsh ,它们通过一
个叫rresvport的调用来获得513-1023中的未用端口号,这个调用从大的数值开始
一个一个试,直到遇到没有被占用的,一般情况下1023 是没有被占用的,也就是
说rresvport一般是返回1023。
________________________________________________________________________
socket pair:就是这种写法:<206.62.226.63.1500 , 198.69.10.2.21>
IP地址后面的数字表示当地的端口号,这就表示一对socket,它有四个元素组成:
本地IP、本地端口号、远端IP、远端端口号,一个socket pair在它所处的网络上
是唯一的,譬如因特网,它可以标识一个唯一确定的TCP连接。
尽管UDP不是面向连接的协议,但我们也可以定义UDP的socket pair,当然,
这没什么意义。
为了理解socket pair,我们考虑这样的情况:server是198.69.10.2的ftpd
进程,它绑定的端口是21,然后,206.62.226.63的一个用户与这个server建立了
连接,它获得的临时端口号是1500,这时,就有了个socket pair:< 198.69.10.
2.21 , 206.62.226.63.1500>,然后问题出来了,198.69.10.2上的端口21不是在
listen吗?那如果新的连接请求来了怎么办?回想那个Daytime的server的源码,
用于listen的socket是listenfd,而与client通信的socket是connfd,这么说吧:
在198.69.10.2上这时确实是有两个看上去一样的socket:IP地址一样,端口号一
样,但是,我们确定一个TCP连接是要一个socket pair,于是,如果有发给198.
69.10.2.21的包,TCP内核会检查包的来源,如果是206.62.226.63.1500来的,就
把包送给已经建立了连接的那个socket,如果不是,就送给那个在listen的socket。
更极端一点,如果server是并发的(ftpd确实是的),在处理第一个连接的时候生
成了一个子进程,然后从206.62.226.63又来了一个连接请求,然后它再开一个子
进程,这个socket pair譬如说是:< 198.69.10.2.21 , 206.62.226.63.1501>,
那么,以后凡是发给198.69.10.2.21的包,198.69.10.2上的TCP内核都会检查来
源,如果是206.62.226.63.1501来的,就交给第二个子进程的connfd,如果是206
.62.226.63.1500来的,就交给第一个子进程的connfd,如果都不是,就交给 --
listenfd。
________________________________________________________________________
IP层所受的物理限制和对TCP的影响:
1、IPv4包最大不能超过65535字节,包括头部,因为长度字段只有16位。
2、IPv6包最大不能超过65575字节,因为它的长度字段不包括头部的40字节。
3、MTU:Maximum Transmission Unit,由链路层决定的最大帧长,Ethernet
的是1500字节,PPP的MTU是可协商的,SLIP的是296字节、、、。
IPv4需要最少有MTU值为68字节的链路层,IPv6要576字节。
4、路径最小MTU,是指从一台主机到另一台主机中间所有链路的最小MTU,现
在这个值一般是Ethernet的1500字节。这个路径最小MTU,是有方向性的,从A到B
的值和从B到A的值可能不一样,因为路由可能不同。
5、当IP要在一个MTU比当前要发送数据的长度小的接口上发送包的时候,IP
就会把数据分段,这些被分段的数据在没有到达最终目的地之前不被重装。
对于IPv4,主机和路由器都可以进行包的分段,而在IPv6中,路由器不允
许对要它转发的包进行分段,而是简单的返回错误信息:Packet too big。(要
说明的是,如果是路由器本身产生的包,譬如由telnetd产生的,因为很多路由器
都要通过局域网来进行配置,直接接终端太麻烦;这些包是可以被路由器分段的)
6、如果“DF”(Don't fragment)被置的话,IPv4中的主机和路由器都不能对
包进行分段,而是返回:fragmentation needed but DF bit set 的ICMP 包或出
错信息。DF位的这个特性被用于找出路径最小MTU,当然IPv6也可以做这件事情了,
因为它的路由器本来就不能分段。
7、最小重装缓冲区(Minimum Reassembly Buffer Size)大小,IPv4中为576字
节,IPv6为1500字节,我们无法得知一个给定的IPv4主机是否能接收一个577 字节
的包。
8、TCP的MSS,已经说过了,这里要指出的是,它的值一般是路径最小MTU减去
TCP头部长度和IP头部长度,譬如,路径最小MTU是Ethernet的1500字节的时候,使
用IPv4的TCP提出的MSS就是1460,如果用的是IPv6的话,就是1440。
________________________________________________________________________
TCP的输出过程的简要描述:
每个TCP socket都有一个发送缓冲,我们可以通过setsockopt的SO_SNDBUF来改
变它的大小,当应用程序调用了write,内核就把所有的数据从应用程序的内存空间
拷贝到socket的发送缓冲,如果空间不够的话(有可能是应用程序的数据太多,同
时socket的发送