TCP/IP

TCP/IP的5层结构

TransmissionControl Protocol/Internet Protocol“传输控制协议/因特网互联协议”TCP/IP实际上是一组协议,它包括上百个各种功能的协议,如:远程登录、文件传输和电子邮件等,而TCP协议和IP协议是保证数据完整传输的两个基本的重要协议。通常说TCP/IPInternet协议族,而不单单是TCPIP

5层的分层体系结构:高层为TCP,把聚集信息或者把文件拆分成小包

低层为IP,处理每个包的地址部分,使这些包正确地到达目的地

(1)       物理层:网络基本硬件,INTEREENT的物理构成,比如PC,互联网服务器,网络设备等,对这些硬件设备和电气特性进行规范

(2)       网络接口层:定义了将数据组成正确帧的规则和在网络中传输帧的规则,帧是一串数据,数据在网络中传输的单位

(3)       互联网层:本层定义了互联网中传输的“信息包”格式,及从一个用户通过一个或多个路由器到最终目标的”信息包”转发机制

(4)       传输层:为两个用户进程间建立、管理和拆除可靠而有效的端到端连接

(5)       应用层:定义了应用程序使用互联网的规则

网络层协议(IP)

为用户和远程计算机提供了信息包的传输方法。

(1)地址解析协议ARP,(2)Internet控制消息协议ICMP(3)Internet协议IP

传输协议层(TCP):

TCP(传输控制协议):可靠的面向连接的协议(需要通话,确定是不是开机,网络是否到达,是否有信号是否接听)

UDP(用户包协议):不可靠的或者说无连接的协议(发短信,只管发出去,不管对方是不是空号,网络是否能到达)

C#的TCP传输有两个策略,一个是发送端的Nagle算法,一个是ACK延迟反馈机制

HTTP连接

HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。
2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。
由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

HTTP是应用层协议,定义的是传输数据的内容的规范
HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP也就一定支持TCP

HTTP支持的是www服务 

传输方式(三次握手):

第一次握手:客户端发送syn(syn=x)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYNack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYNACK,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过四次握手(过程就不细写了,就是服务器和客户端交互,最终确定断开)

 

 

1.     客户端发送synseq=x),进入SYN_SEND,等服务器确认

2.     服务器收到syn,确认、发送ack(x+1),发送syn(seq=y)———发送syn+ack包,进入SYN_RECV状态

3.     客户端收到syn+ack,确认、发送acky+1),客户端和服务器进入ESTABLISHED状态,完成三次握手

4.     握手过程传送包里不含数据,三次握手完毕才开始传数据。在任何一方主动关闭连接之前,连接一直保持下去。服务器和客户端可以主动发起断开请求,需要第四次握手。

 

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

 

 TCP短连接

我们模拟一下TCP短连接的情况,clientserver发起连接请求,server接到请求,然后双方建立连接。clientserver发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。为什么呢,一般的server不会回复完client后立即关闭连接的,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在client/server间传递一次读写操作

短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段

3.TCP长连接

接下来我们再模拟一下长连接的情况,clientserver发起连接,server接受client连接,双方建立连接。Clientserver完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

首先说一下TCP/IP详解上讲到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务器端检测到这种半开放的连接。

如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

1.   客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。

2.   客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。

3.   客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。

4.   客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。

 

SOCKTE原理

套接字(socket)概念(协议,本地主机ip地址,本地进程协议端口,远地主机ip地址,远地进程协议端口)

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口

 

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

建立socket连接

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

 

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求(clientSocket必须知道serverSocket的地址和端口号,进行扫描发出连接请求

连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

 

注:在连接确认阶段:服务器socket即使在和一个客户端socket建立连接后,还在处于监听状态,仍然可以接收到其他客户端的连接请求,这也是一对多产生的原因。(服务器可以监听多个客户端的原因)

下图简单说明连接过程:

可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCPUDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

 

图1. TCP编程基本模型

    图2是利用UDP通信时的编程基本模型,这个模型较为简单,但是应用极为广泛,相比TCP而言,我本人觉得利用UDP通信是一门更为高深的技术,因为它是无连接的,换言之,它的效率与灵活度就更高些。

图2. UDP编程基本模型

创建端地址

在C#中我们利用IPEndPoint类来表示一个端地址,

1.  IPEndPoint localEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"),6666);  

绑定一个端地址

已经创建了一个端地址,也构造了套接字(TCPClient类),那么如何将二者绑定起来呢?在建立TCPClient的时候我们其实就可以绑定端地址了。如果你使用的TCPClient tcp_Client=new TCPClient()的构造函数来创建的TCPClient,那么系统会认为你没有人为的制定端地址,而会自动帮你制定端地址,在创建客户端的TCPClient时我们常常这样做,因为我们不关心客户端的端地址。如果是服务器监听呢?在服务器监听时我们会使用例外一个类,叫做TCPListener,接下来我会讲到。

1.  TcpClient tcp_Client = new TcpClient(localEP);  

监听套接字

       在传统的socket编程中,我们创建一个套接字,然后把它绑定到一个端地址,而后调用Listen()来监听套接字。而在C#中,我们利用TCPListener来帮我们完成这些工作。

1.  IPEndPointlocalEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"),6666);  

2.  TcpListenerListener = new TcpListener(localEP);  

3.  Listener.Start(10); 

我们首先创建需要绑定的端地址,而后创建监听类,并利用其构造函数将其绑定到端地址,然后调用Start(int number)方法来真正实施监听。这与我们传统的socket编程不同。以前我们都是先创建一个socket,然后再创建一个sockaddr_in的结构体。我想你应该开始感受到了C#的优势了,它帮我们省去了很多低级、繁琐的工作,让我们能够真正专注于我们的软件架构和设计思想。

接受客户端的连接

1.  TcpClient remoteClient =Listener.AcceptTcpClient();  

   类似于accept函数来返回一个socket,利用TCPListener类的AcceptTcpClient方法我们可以得到一个与客户端建立了连接的TCPClient类,而由TCPClient类来处理以后与客户端的通信工作。我想你应该开始理解为什么会存在TCPClient和TCPListener两个类了。这两个类的存在有着更加明细的区分,让监听和后续的通信真正分开,让程序员也更加容易理解和使用了。

监听是一个非阻塞的操作(Listener.Start()),而接受连接是一个阻塞操作(Listener.AcceptTcpClient)。

判断ip地址是否合法

IPAddress.TryParse(ip地址,out ip地址)

注意:

IPAddress ip;

IPAddress.TryParse("8", out ip);

Console.WriteLine(ip);

执行上面的代码,会得到“0.0.0.8”的输出。 
但是实际使用中,往往这不是我们想要的。使用该方法校验用户输入的ip地址就不太合理。一般来说,ip地址格式应该是”xxx.xxx.xxx.xxx”,而使用IPAddress类的静态方法TryParse校验ip地址时,随意输入一个整形字符串,也校验通过了。简直让人一脸懵逼。对于这种需求下,可以使用正则表达式来校验ip地址。下面是完整代码。

publicstaticboolIPCheck(string IP)

{

    return Regex.IsMatch(IP, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$");

}

那么类IPAddress的静态方法TryParse的判断逻辑是怎样的呢? 
TryParse方法先判断字符串是否能转换成uint类型数据,不能则返回false,转换成功则将uint数据转换成32位二进制数据,每八位二进制数据转换成十进制数,便转换成了最终的ip地址。像例子中的8转换成32位二进制数据为00000000000000000000000000001000,转换成ip地址为0.0.0.8。

 

正确的格式:

现在的IP网络使用32位地址,以点分十进制表示,如192.168.0.1。地址格式为:

IP地址=网络地址+主机地址 或 IP地址=网络地址+子网地址+主机地址

 

IP地址分为五类,A类保留给政府机构,B类分配给中等规模的公司,C类分配给任何需要的人,D类用于组播,E类用于实验,各类可容纳的地址数目不同。
  A、B、C三类IP地址的特征:当将IP地址写成二进制形式时,A类地址的第一位总是0,B类地址的前两位总是10,C类地址的前三位总是110。
   1. A类地址
  (1)A类地址第1字节为网络地址,其它3个字节为主机地址
  (2)A类地址范围:1.0.0.1—126.255.255.254
  (3)A类地址中的私有地址和保留地址:
  ① 10.X.X.X是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。
  范围(10.0.0.0-10.255.255.255)
  ② 127.X.X.X是保留地址,用做循环测试用的。
  2. B类地址
  (1) B类地址第1字节和第2字节为网络地址,其它2个字节为主机地址。
  (2) B类地址范围:128.0.0.1—191.255.255.254。
  (3) B类地址的私有地址和保留地址
  ① 172.16.0.0—172.31.255.255是私有地址
  ② 169.254.X.X是保留地址。如果你的IP地址是自动获取IP地址,而你在网络上又没有找到可用的DHCP服务器。就会得到其中一个IP。
   3. C类地址
  (1)
C类地址第1字节、第2字节和第3个字节为网络地址,第4个个字节为主机地址。另外第1个字节的前三位固定为110。
  (2)C类地址范围:192.0.0.1—223.255.255.254。
  (3) C类地址中的私有地址:
  192.168.X.X是私有地址。(192.168.0.0-192.168.255.255)如:192.168.5.103(网络地址.主机地址)——内网ip
  4. D类地址
  (1) D类地址不分网络地址和主机地址,它的第1个字节的前四位固定为1110。
  (2) D类地址范围:224.0.0.1—239.255.255.254
   5. E类地址
  (1) E类地址不分网络地址和主机地址,它的第1个字节的前四位固定为1111。
  (2) E类地址范围:240.0.0.1—255.255.255.254特殊的IP地址(1)受限广播地址

 

 

 

 

代码:

<服务器端>

int port = 6000;

           string host ="127.0.0.1";

            IPAddress ip =IPAddress.Parse(host);

                     //IPEndPoint类包含应用程序连接到主机上的服务所需的主机端口信息,通过组合服务的主机IP地址和端口号,IPEndPoint类形成到服务的连接点

                 //ip:ip地址(IPAddress),port端口号(int)

           IPEndPoint ipe = new IPEndPoint(ip, port);

                     //Socket网络通信接口号

           SocketsSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);

           sSocket.Bind(ipe);

           sSocket.Listen(0);

           Console.WriteLine("监听已经打开,请等待");

 

           //receive message

           Socket serverSocket = sSocket.Accept();

 

           Console.WriteLine("连接已经建立");

           string recStr ="";

           byte[] recByte =new byte[4096];

           int bytes = serverSocket.Receive(recByte, recByte.Length, 0);

           recStr +=Encoding.ASCII.GetString(recByte,0, bytes);

 

           //send message

           Console.WriteLine("服务器端获得信息:{0}",recStr);

           string sendStr = "send to client :hello";

           byte[] sendByte =Encoding.ASCII.GetBytes(sendStr);

           serverSocket.Send(sendByte, sendByte.Length, 0);

           serverSocket.Close();

           sSocket.Close();

 

 

<客户端>1. 端口号(IPAddress类,int port)2.IP地址(IPAddress类)3.(Socket类)4. (clientSocket.Connect)5. 发送信息clientSocket.Send 6. int bytes =clientSocket.Receive接收信息

int port = 6000;

           string host = "127.0.0.1";//服务器端ip地址

           IPAddress ip = IPAddress.Parse(host);

                     //ipip地址(IPAddress),port端口号(int

            IPEndPoint ipe = new IPEndPoint(ip,port);

                     //Socket网络通信接口号

           Socket clientSocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

           clientSocket.Connect(ipe);

 

           //send message

           string sendStr = "send to server : hello,ni hao";

           byte[] sendBytes = Encoding.ASCII.GetBytes(sendStr);

           clientSocket.Send(sendBytes);

 

           //receive message

           string recStr = "";

           byte[] recBytes = new byte[4096];

           int bytes = clientSocket.Receive(recBytes, recBytes.Length, 0);

           recStr += Encoding.ASCII.GetString(recBytes, 0, bytes);

           Console.WriteLine(recStr);

           clientSocket.Close();

 

客户端连接代码

(1)    连接按钮

       /// <summary>

        /// 连接Server端

        ///</summary>

        ///<param name="sender"></param>

        ///<param name="e"></param>

        privatevoid ConnectServer_Btn_Click(object sender, EventArgs e)

        {

           #region Check Information

 

            if (Password_Textbox.Text == "" ||Username_Textbox.Text == "" || ServerIP_Textbox.Text == ""|| ServerPort_Textbox.Text == "")

            {

               MessageBox.Show("Please fill all information!");

               return;

            }

            else

            {

               //判断ip地址是否合法

               IPAddress ip;

               if(IPAddress.TryParse(TCPIPClientClass.ServerIP,out ip))

               {         

     

               }

               else

               {

                   MessageBox.Show("Illeagl IP!");

                   return;

               }

               int port;

               if (int.TryParse(TCPIPClientClass.ServerPort, out port))

               {

 

               }

               else

                {

                   MessageBox.Show("Illeagal Port!");

                   return;                   

               }

            }

 

           #endregion

 

           #region Connect to Server

 

            //Sockte-客户端

           TCPIPClientClass.ClientSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);

            //IPEndPoint

           TCPIPClientClass.ServerIPEndPoint = newIPEndPoint(IPAddress.Parse(TCPIPClientClass.ServerIP), Convert.ToInt32(TCPIPClientClass.ServerPort));

 

            try

            {

                //客户端Socket连接--客户端Socket.Connect--连接服务器IP+服务器端口

             TCPIPClientClass.ClientSocket.Connect(TCPIPClientClass.ServerIPEndPoint);

                //客户端Socket异步模式接收--Socket.BeginReceive

               TCPIPClientClass.ClientSocket.BeginReceive(TCPIPClientClass.MsgBuffer,0, TCPIPClientClass.MsgBuffer.Length, 0, newAsyncCallback(TCPIPClientClass.ClientReceiveProcess), null);

 

               this.ConnectServer_Btn.Enabled = false;

               this.DisconnectServer_Btn.Enabled = true;

               this.TCPIPLED_Btn.BackColor = Color.Red;

               this.ConnectStatus_Textbox.Text = "Client is connected to serverat" + DateTime.Now.ToString() + ".";

 

                AccessDataBase myacc = newAccessDataBase();

               myacc.SaveTCPIPIPInformation(1);

            }

           catch (Exception ex)

            {

               MessageBox.Show(ex.Message);

               this.TCPIPLED_Btn.BackColor = Color.Transparent;

            }

           

           #endregion

 

        }

(2)    接收信息线程

         /// <summary>

        /// 客户端接收数据线程

        ///</summary>

        ///<param name="AR"></param>

        publicstatic void ClientReceiveProcess(IAsyncResult AR)

        {

            try

           {               

               string recvStr = "";

                //接收字符串的长度:Socket.EndReceive

                int REnd =TCPIPClientClass.ClientSocket.EndReceive(AR);

                //接收的字符串

                recvStr +=Encoding.UTF8.GetString(TCPIPClientClass.MsgBuffer, 0, REnd);

               //字符串为“exit”--停止接收

               if (recvStr == "exit" || recvStr == "")

               {

                    Control.CheckForIllegalCrossThreadCalls= false;

                   TCPIPClientClass.txtB_ConnectStatus.Text = "Client is disconnect toSever at " + DateTime.Now.ToString() + ", because the Server isstopped.";

                   //如果Socket已经连接

                    //Socket.Shutdown

                   //Socket.Disconnect

                   //Socket.Close

                   if (TCPIPClientClass.ClientSocket.Connected)

                   {

                       TCPIPClientClass.ClientSocket.Shutdown(SocketShutdown.Both);

                       TCPIPClientClass.ClientSocket.Disconnect(false);

                   }

                   TCPIPClientClass.ClientSocket.Close();

                   TCPIPClientClass.btn_ConnectServer.Enabled =true;

                   TCPIPClientClass.btn_DisconnectServer.Enabled = false;

                   TCPIPClientClass.btn_LED.BackColor = Color.Transparent;

               }

               else

                {

                   //如果串口使能助手打开

                   if (TCPIPClientClass.AssisantEnableFlag)

                   {

                        //如果HEX显示

                        if(TCPIPClientClass.ReceiveHexDisplayFlag)

                        {

                            string getstr, buf;

                            buf =Encoding.GetEncoding("GB2312").GetString(TCPIPClientClass.MsgBuffer,0, REnd);

                            getstr ="";

                            foreach (byte b inbuf)

                            {

                                getstr = getstr+ b.ToString("X2") + "";

                            }

                           TCPIPClientClass.txtB_ClientDataRecArea.AppendText(">>  " + getstr + "\r\n");

                        }

                        //字符串显示

                        else

                        {

                           TCPIPClientClass.txtB_ClientDataRecArea.AppendText(">> "+Encoding.GetEncoding("GB2312").GetString(TCPIPClientClass.MsgBuffer,0,REnd));

                        }

                   }

                   //重新开始客户端Socket的异步接收线程

                    //客户端Socket异步模式接收--Socket.BeginReceive

                   TCPIPClientClass.ClientSocket.BeginReceive(TCPIPClientClass.MsgBuffer,0, TCPIPClientClass.MsgBuffer.Length, 0, newAsyncCallback(ClientReceiveProcess), null);

                }

            }

           catch

           {           

            }

        }

 

服务器监听代码

TCPIP如何保证包的顺序传输

我和大家一起讨论下TCP在保证可靠传输数据的前提下,是怎样对传输的数据进行顺序化操作的。
大家都知道,TCP提供了最可靠的数据传输,它给发送的每个数据包做顺序化(这看起来非常烦琐),然而,如果TCP没有这样烦琐的操作,那么,可能会造成更多的麻烦。如造成数据包的重传、顺序的颠倒甚至造成数据包的丢失。
那么,TCP具体是通过怎样的方式来保证数据的顺序化传输呢?

主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。

 

具体步骤如下:

(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;

 

(2)并为每个已发送的数据包启动一个超时定时器;

 

(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;

 

(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。

 

(5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

IP地址

IP地址是IP协议的重要组成部分,它可以识别接入互联网中的任意一台设备。在IP接力中,我们已经看到,IP包的头部写有出发地和目的地的IP地址。IP包上携带的IP地址和路由器相配合,最终允许IP包从互联网的一台电脑传送到另一台。

IP接力中,我们是以IPv4为例说明IP包的格式的。IPv4IPv6是先后出现的两个IP协议版本。IPv4的地址就是一个320/1序列,比如11000000 00000000 0000000 00000011。为了方便人类记录和阅读,我们通常将320/1分成48位序列,并用10进制来表示每一段(这样,一段的范围就是0255),段与段之间以.分隔。比如上面的地址可以表示成为192.0.0.3IPv6地址是1280/1序列,它也按照8位分割,以16进制来记录每一段(使用16进制而不是10进制,这能让写出来的IPv6地址短一些),段与段之间以:分隔。

IP地址的分配

IP地址的分配是一个政策性的问题。1ICANN(the Internet Corporation for AssignedNames and Numbers)Internet的中心管理机构。ICANNIANA(InternetAssigned Numbers Authourity)部门负责IP地址分配给5个区域性的互联网注册机构(RIRReginal Internet Registry),比如APNIC,它负责亚太地区的IP分配。2然后RIR将地址进一步分配给当地的ISP(InternetService Provider),比如中国电信和中国网通。3ISP再根据自己的情况,将IP地址分配给机构或者直接分配给用户,比如将A类地址分配给一个超大型机构,而将C类地址分配给一个网吧。机构可以进一步在局域网内部分配IP地址给各个主机(A/B/C类地址请参阅IP接力)

5RIR的分管区域

并不是所有的地址都会被分配。一些地址被预留,用于广播、测试、私有网络使用等。这些地址被称为专用地址(special-useaddress)。你可以查询RFC5735来了解哪些地址是专用地址。

(RFCRequest For CommentsRFC是一系列的技术文档,用于记录Internet相关的技术和协议规定。每一个RFC文件都有一个固定的编号。它们是互联网的一个重要财产。你可以通过 http://www.rfc-editor.org/ 来查找RFC文件)

IPv4地址耗尽

由于IPv4协议的地址为32,所以它可以提供232, 也就是大约40亿个地址。如果地球人每人一个IP地址的话,IPv4地址已经远远不够。更何况,人均持有的入网设备可能要远多于一个,下图中显示了一个家庭对IP地址的需求,这种需求量已经相当常见了:

We need more IP address

下图显示了各大洲RIRIPv4地址耗尽日期 (IANA已经将所有的IP分配给各个RIR)

5RIR区域的预计耗尽日期

 

尽管一些技术措施(比如NAT技术,我会在其他文章中深入NAT)减缓了情况的紧急程度,但IPv4地址耗尽的一天终究还是会很快到来。很明显,我们需要更多的IP地址,以满足爆炸式增长的互联网设备对IP地址的需求。

 

更长=更好

IPv6协议的地址最重要的改进就是:加长IPv6的地址为128。准确的说,IPv44,294,967,296个地址,而IPv6

340,282,366,920,938,463,374,607,431,768,211,456

个地址。这是怎样一个概念呢?我们可以大概计算一下

地球表面积大约为510,067,866,000,000平方米。在一平方厘米(大约是指甲盖大小)的面积内,我们可以有6.67x1016IP地址!所以在短期的时间内,我们应该不会看到IPv6被用尽的尴尬。(不排除在未来计算机以分子尺寸出现,那么我们就会有IPv6耗尽危机了)

所以,为了解决IPv4地址耗尽危机,这就是结论:

IPv4IPv6头部的对比

我们已经在IP接力中介绍过,一个IP包分为头部(header)数据(payload/data)两部分。头部是为了实现IP通信必须的附加信息,数据是IP通信所要传送的信息。

 黄色区域 (同名区域)

我们看到,三个黄色区域跨越了IPv4IPv6Version(4)用来表明IP协议版本,是IPv4还是IPv6(IPv4, Version=0100; IPv6,Version=0110)Source AdrresssDestination Address分别为发出地和目的地的IP地址

蓝色区域(名字发生变动的区域)

Time to Live 存活时间(Hop Limit in IPv6)Time to Live最初是表示一个IP包的最大存活时间:如果IP包在传输过程中超过Time to Live,那么IP包就作废。后来,IPv4的这个区域记录一个整数(比如30),表示在IP包接力过程中最多经过30个路由接力,如果超过30个路由接力,那么这个IP包就作废。IP包每经过一个路由器,路由器就给Time to Live减一。当一个路由器发现Time to Live0时,就不再发送该IP包。IPv6中的Hop Limit区域记录的也是最大路由接力数,与IPv4的功能相同。Time to Live/Hop Limit避免了IP包在互联网中无限接力。

Type of Service 服务类型(Traffic Class in IPv6)Type of Service最初是用来给IP包分优先级,比如语音通话需要实时性,所以它的IP包应该比Web服务的IP包有更高的优先级。然而,这个最初不错的想法没有被微软采纳。在Windows下生成的IP包都是相同的最高优先级,所以在当时造成LinuxWindows混合网络中,LinuxIP传输会慢于Windows (仅仅是因为Linux更加守规矩!)。后来,Type of Service被实际分为两部分:Differentiated Service Field (DS, 6)Explicit Congestion Notification (ECN, 2),前者依然用来区分服务类型,而后者用于表明IP包途径路由的交通状况。IPv6Traffic Class也被如此分成两部分。通过IP包提供不同服务的想法,并针对服务进行不同的优化的想法已经产生很久了,但具体做法并没有形成公认的协议。比如ECN区域,它用来表示IP包经过路径的交通状况。如果接收者收到的ECN区域显示路径上的很拥挤,那么接收者应该作出调整。但在实际上,许多接收者都会忽视ECN所包含的信息。交通状况的控制往往由更高层的比如TCP协议实现。

Protocol 协议(Next Header in IPv6)Protocol用来说明IPPayload部分所遵循的协议,也就是IP包之上的协议是什么。它说明了IP包封装的是一个怎样的高层协议包(TCP? UDP?)

Total Length, 以及IPv6Payload Length的讨论要和IHL区域放在一起,我们即将讨论。

红色区域 (IPv6中删除的区域)

我们看一下IPv4IPv6的长度信息。IPv4头部的长度。在头部的最后,是options。每个options32位,是选填性质的区域。一个IPv4头部可以完全没有options区域。不考虑options的话,整个IPv4头部有20 bytes(上面每行为4 bytes)。但由于有options的存在,整个头部的总长度是变动的。我们用IHL(Internet Header Length)来记录头部的总长度,用TotalLength记录整个IP包的长度。IPv6没有options,它的头部是固定的长度40bytes,所以IPv6中并不需要IHL区域。Payload Length用来表示IPv6的数据部分的长度。整个IP包为40 bytes + Payload Length

IPv4中还有一个Header Checksum区域。这个checksum用于校验IP包的头部信息。Checksum与之前在小喇叭中提到的CRC算法并不相同。IPv6则没有checksum区域。IPv6包的校验依赖高层的协议来完成,这样的好处是免去了执行checksum校验所需要的时间,减小了网络延迟 (latency)

Identificationflagsfragment offset,这三个包都是为碎片化(fragmentation)服务的。碎片化是指一个路由器将接收到的IP包分拆成多个IP包传送,而接收这些碎片的路由器或者主机需要将碎片重新组合(reassembly)成一个IP包。不同的局域网所支持的最大传输单元(MTU, Maximum Transportation Unit)不同。如果一个IP包的大小超过了局域网支持的MTU,就需要在进入该局域网时碎片化传输(就好像方面面面饼太大了,必须掰碎才能放进碗里)。碎片化会给路由器和网络带来很大的负担。最好在IP包发出之前探测整个路径上的最小MTUIP包的大小不超过该最小MTU,就可以避免碎片化。IPv6在设计上避免碎片化。每一个IPv6局域网的MTU都必须大于等于1280 bytesIPv6的默认发送IP包大小为1280 bytes

绿色区域 (IPv6新增区域)

Flow LabelIPv6中新增的区域。它被用来提醒路由器来重复使用之前的接力路径。这样IP包可以自动保持出发时的顺序。这对于流媒体之类的应用有帮助。Flow label的进一步使用还在开发中。

我尽力

IP协议在产生时是一个松散的网络,这个网络由各个大学的局域网相互连接成的,由一群碰头垢面的Geek维护。所以,IP协议认为自己所处的环境是不可靠(unreliable)的:诸如路由器坏掉、实验室失火、某个PhD踢掉电缆之类的事情随时会发生。

这样的凶险环境下,IP协议提供的传送只能是我尽力” (best effort)式的。所谓的我尽力,其潜台词是,如果事情出错不要怪我,我只是答应了尽力,可没保证什么。所以,如果IP包传输过程中出现错误(比如checksum对不上,比如交通太繁忙,比如超过Time to Live),根据IP协议,你的IP包会直接被丢掉。Game Over, 不会再有进一步的努力来修正错误。Best effortIP协议保持很简单的形态。更多的质量控制交给高层协议处理,IP协议只负责有效率的传输。

效率优先也体现在IP包的顺序(order)上。即使出发地和目的地保持不变,IP协议也不保证IP包到达的先后顺序我们已经知道,IP接力是根据routing table决定接力路线的。如果在连续的IP包发送过程中,routing table更新(比如有一条新建的捷径出现),那么后发出的IP包选择走不一样的接力路线。如果新的路径传输速度更快,那么后发出的IP包有可能先到。这就好像是多车道的公路上,每辆车都在不停变换车道,最终所有的车道都塞满汽车。这样可以让公路利用率达到最大。

插队

IPv6中的Flow Label可以建议路由器将一些IP包保持一样的接力路径。但这只是建议,路由器可能会忽略该建议。

Header Checksum算法

Header Checksum区域有16位。它是这样获得的,从header取得除checksum之外的0/1序列,比如:

9194 8073 0000 4000 4011 C0A8 0001 C0A800C7 (十六进制hex, 这是一个为演示运算过程而设计的header)

按照十六位(也就是4hex)分割整个序列。将分割后的各个4hex累积相加。如果有超过16位的进位出现,则将进位加到后16位结果的最后一位:

 Binary               Hex

 1001000110010100      9194

+1000000001110011      8073

  ----------------

1 0001001000000111    11207

+               1

  ----------------

 0001001000001000      1208
上面的计算叫做one's complement sum。求得所有十六位数的和,

one's complement sum(4500, 0073, 0000,4000, 4011, C0A8, 0001, C0A8, 00C7) = 1433

然后,将1433的每一位取反(0->1, 1->0)就得到checksumEBCC

这样,我们的header就是:

9194 8073 0000 4000 4011 EBCC C0A80001 C0A8 00C7

IP包的接收方在接收到IP包之后,可以求上面各个16位数的one's complement sum,应该得到FFFF。如果不是FFFF,那么header是不正确的,整个IP包会被丢弃。

网络层(network layer)是实现互联网的最重要的一层。正是在网络层面上,各个局域网根据IP协议相互连接,最终构成覆盖全球的Internet。更高层的协议,无论是TCP还是UDP,必须通过网络层的IP数据包(datagram)来传递信息。操作系统也会提供该层的socket,从而允许用户直接操作IP包。

IP数据包是符合IP协议的信息(也就是0/1序列),我们后面简称IP数据包为IPIP包分为头部(header)数据(Data)两部分。数据部分是要传送的信息,头部是为了能够实现传输而附加的信息(这与以太网帧的头部功能相类似,如果对帧感到陌生,可参看小喇叭一文)

 IP发送

IP包的格式

IP协议可以分为IPv4IPv6两种。IPv6是改进版本,用于在未来取代IPv4协议。出于本文的目的,我们可以暂时忽略两者的区别,只以IPv4为例。下面是IPv4的格式

IPv4我们按照4 bytes将整个序列折叠,以便更好的显示

与帧类似,IP包的头部也有多个区域。我们将注意力放在红色的发出地(source address)目的地(destination address)。它们都是IP地址IPv4的地址为4 bytes的长度(也就是32)。我们通常将IPv4的地址分为四个十进制的数,每个数的范围为0-255,比如192.0.0.1就是一个IP地址。填写在IP包头部的是该地址的二进制形式

IP地址是全球地址,它可以识别"社区"(局域网)"房子"(主机)。这是通过将IP地址分类实现的。

IP class   From         To               Subnet Mask

A          1.0.0.0       126.255.255.255   255.0.0.0

B          128.0.0.0     191.255.255.255    255.255.0.0

C          192.0.0.0     223.255.255.255   255.255.255.0

每个IP地址的32位分为前后两部分,第一部分用来区分局域网,第二个部分用来区分该局域网的主机。子网掩码(Subnet Mask)告诉我们这两部分的分界线,比如255.0.0.0(也就是81240)表示8位用于区分局域网,后24位用于区分主机。由于ABC分类是已经规定好的,所以当一个IP地址属于B类范围时,我们就知道它的前16位和后16位分别表示局域网和主机。

网卡与路由器

邮差与邮局中说,IP地址是分配给每个房子(计算机)"邮编"。但这个说法并不精确IP地址实际上识别的是网卡(NIC, Network Interface Card)。网卡是计算机的一个硬件,它在接收到网路信息之后,将信息交给计算机(处理器/内存)。当计算机需要发送信息的时候,也要通过网卡发送。一台计算机可以有不只一个网卡,比如笔记本就有一个以太网卡和一个WiFi网卡。计算机在接收或者发送信息的时候,要先决定想要通过哪个网卡。

NIC

 

路由器(router)实际上就是一台配备有多个网卡的专用电脑。它让网卡接入到不同的网络中,这样,就构成在邮差与邮局中所说的邮局比如下图中位于中间位置的路由器有两个网卡,地址分别为199.165.145.17199.165.146.3。它们分别接入到两个网络:199.165.145199.165.146

IP包接力

IP包的传输要通过路由器的接力。每一个主机和路由中都存有一个路由表(routing table)路由表根据目的地的IP地址,规定了等待发送的IP包所应该走的路线。就好像下图的路标,如果地址是东京,那么请转左;如果地址是悉尼,那么请向右。

比如我们从主机145.17生成发送到146.21IP包:铺开信纸,写好信的开头(剩下数据部分可以是TCP包,可以是UDP包,也可以是任意乱写的字,我们暂时不关心),注明目的地IP地址(199.165.146.21)和发出地IP地址(199.165.145.17)。主机145.17随后参照自己的routing table,里面有三行记录:

145.17 routing table (Genmask为子网掩码,Iface用于说明使用哪个网卡接口)

Destination       Gateway             Genmask            Iface

199.165.145.0       0.0.0.0            255.255.255.0       eth0

0.0.0.0           199.165.145.17     0.0.0.0            eth0

这里有两行记录。

第一行表示,如果IP目的地是199.165.145.0,可以直接送达这个网络的主机,那么只需要自己在eth0上的网卡直接传送(本地社区:直接送达),不需要前往router(Gateway 0.0.0.0 = “本地送信”)

第二行表示所有不符合第一行的IP目的地,都应该送往Gateway 199.165.145.17,也就是中间router接入在eth0的网卡IP地址(邮局在eth0的分支)

我们的IP包目的地为199.165.146.21不符合第一行,所以按照第二行,发送到中间的router主机145.17会将IP包放入帧的payload,并在帧的头部写上199.165.145.17对应的MAC地址,这样,就可以按照小喇叭中的方法在局域网中传送了。

 

中间的router在收到IP包之后(实际上是收到以太协议的帧,然后从帧中的payload读取IP),提取目的地IP地址,然后对照自己的routing table

Destination       Gateway             Genmask            Iface

199.165.145.0     0.0.0.0            255.255.255.0       eth0

199.165.146.0     0.0.0.0            255.255.255.0       eth1

0.0.0.0           199.165.146.8      0.0.0.0            eth1

从前两行我们看到,由于router横跨eth0和eth1两个网络,它可以直接通过eth0和eth1上的网卡直接传送IP包。

第三行表示,如果是前面两行之外的IP地址,则需要通过eth1,送往199.165.146.8(右边的router)。

我们的目的地符合第二行,所以将IP放入一个新的帧中,

在帧的头部写上199.165.146.21的MAC地址,直接发往主机146.21。

(在Linux下,可以使用$route -n来查看routing table)

 

IP包可以进一步接力,到达更远的主机。IP包从主机出发,根据沿途路由器的routing table指导,在router间接力。IP包最终到达某个router,这个router与目标主机位于一个局域网中,可以直接建立连接层的通信。最后,IP包被送到目标主机。这样一个过程叫做routing(我们就叫IP包接力好了,路由这个词实在是混合了太多的意思)。
整个过程中,IP包不断被主机和路由封装入帧(信封)并拆开,然后借助连接层,在局域网的各个NIC之间传送帧。整个过程中,我们的IP包的内容保持完整,没有发生变化。最终的效果是一个IP包从一个主机传送到另一个主机。利用IP包,我们不需要去操心底层(比如连接层)发生了什么。

 

 

ARP协议

在上面的过程中,我们实际上假设了,每一台主机和路由都能了解局域网内的IP地址和MAC地址的对应关系,这是实现IP包封装(encapsulation)到帧的基本条件。IP地址与MAC地址的对应是通过ARP协议传播到局域网的每个主机和路由。每一台主机或路由中都有一个ARP cache,用以存储局域网内IP地址和MAC地址如何对应。

ARP协议(ARP介于连接层和网络层之间,ARP包需要包裹在一个帧中)的工作方式如下:主机会发出一个ARP包,该ARP包中包含有自己的IP地址和MAC地址。通过ARP包,主机以广播的形式询问局域网上所有的主机和路由:我是IP地址xxxx,我的MAC地址是xxxx,有人知道199.165.146.4MAC地址吗?拥有该IP地址的主机会回复发出请求的主机:哦,我知道,这个IP地址属于我的一个NIC,它的MAC地址是xxxxxx。由于发送ARP请求的主机采取的是广播形式,并附带有自己的IP地址和MAC地址,其他的主机和路由会同时检查自己的ARP cache,如果不符合,则更新自己的ARPcache。

这样,经过几次ARP请求之后,ARP cache会达到稳定。如果局域网上设备发生变动,ARP重复上面过程。

(在Linux下,可以使用$arp命令来查看ARP的过程。ARP协议只用于IPv4。IPv6使用Neighbor Discovery Protocol来替代ARP的功能。)

 

Routing Table的生成

我们还有另一个假设,就是每个主机和路由上都已经有了合理的routing table。这个routint table描述了网络的拓扑(topology)结构。如果你了解自己的网络连接,可以手写自己主机的routing table。但是,一个路由器可能有多个出口,所以routing table可能会很长。更重要的是,周围连接的其他路由器可能发生变动(比如新增路由器或者路由器坏掉),我们就需要routing table能及时将交通导向其他的出口。我们需要一种更加智能的探测周围的网络拓扑结构,并自动生成routing table。

我们以北京地铁为例子。如果从机场前往朝阳门,那么可以采取2号航站楼->>三元桥->>东直门->>朝阳门。2号航站楼和朝阳门分别是出发和目的主机。而三元桥和东直门为中间的两个router。如果三元桥->>东直门段因为维修停运,我们需要更改三元桥的routing table,从而给前往朝阳门的乘客(IP包)指示:请走如下路线三元桥->>芍药居。然后依照芍药居的routingtable前往朝阳门(芍药居->>东直门->>朝阳门)。

 

 

一种用来生成routing table的协议是RIP(Routing Information Protocol)。它通过距离来决定routing table,所以属于distance-vector protocol。对于RIP来说,所谓的距离是从出发地到目的地途径的路由器数目(hop number)。比如上面从机场到朝阳门,按照2号航站楼->>三元桥->>东直门->>朝阳门路线,途径两个路由器,距离为2。我们最初可以手动生成三元桥的routing table。随后,根据RIP协议,三元桥向周围的路由器和主机广播自己前往各个IP的距离(比如到机场=0,团结湖=0,国贸=1,望京西=1,建国门=2)。收到RIP包的路由器和主机根据RIP自己到发送RIP包的主机的距离,算出自己前往各个IP的距离。东直门与三元桥的距离为1。东直门收到三元桥的RIP包(到机场的距离为0),那么东直门途径三元桥前往机场的距离为1+0=1。如果东直门自己的RIP记录都比这个远(比如东直门->>芍药居->>三元桥->>机场 = 2)。那么东直门更改自己的routing table:前往机场的交通都发往三元桥而不是芍药居。如果东直门自身的RIP记录并不差,那么东直门保持routing table不变。上述过程在各个点不断重复RIP广播/计算距离/更新routing table的过程,最终所有的主机和路由器都能生成最合理的路径(merge)。

(RIP的基本逻辑是:如果A距离B为6,而我距离A为1,那么我途径A到B的距离为7)

RIP出于技术上的原因(looping hops),认为距离超过15的IP不可到达。所以RIP更多用于互联网的一部分(比如整个中国电信的网络)。这样一个互联网的部分往往属于同一个ISP或者有同一个管理机构,所以叫做自治系统(AS,autonomous system)。自治系统内部的主机和路由根据通向外部的边界路由器来和其它的自治系统通信。各个边界路由器之间通过BGP(Border Gateway Protocol)来生成自己前往其它AS的routing table,而自治系统内部则参照边界路由器,使用RIP来决定routing table。BGP的基本工作过程与RIP类似,但在考虑距离的同时,也权衡比如政策、连接性能等其他因素,再决定交通的走向(routing table)。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值