CSDN博客

img ed9er

stevens那本socket的第一部分的简要翻译

发表于2001/3/12 14:54:00  852人阅读

 

以前被流放到乡下,无聊时翻的,昨天从盘上翻出来,觉得从来没在网上贴过,埋没了可惜,就灌到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/r/n", 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的发送

 

0 0

相关博文

我的热门文章

img
取 消
img