网络开发(TCP/IP && UDP && Socket)

2016-10-30 » iOS开发基础

一、网络通信三要素

通过 “IP” 找服务器,通过 “端口” 找进程, 通过 “协议” 确定如何传输数据

IP地址(主机名)

端口号

用于标示进程的逻辑地址,不同进程的标示
有效端口:0~65535
其中 0~1024由系统使用或者保留端口
开发中不要使用 1024 以下的端口

注意 : 跟HTTP相关的端口一定是80,服务器上有个进程是专门处理HTTP请求的,端口号是80

传输协议

TCP(传输控制协议) 相当于打电话,必须先建立好链接才能传输数据

UDP(数据报文协议) 相当于发电报,不用关心对方是否能够收到,不太安全

HTTP协议底层是基于TCP/IP协议的,网络传输协议在传输层选择的是TCP/IP协议

二、TCP/IP协议

1、TCP/IP 协议介绍以及基本协议结构

1、TCP(Transmission Control Protocol:传输控制协议)

TCP并不是基于UDP协议构建的,和UDP协议一样是基于IP协议构建的。

主要解决问题

1、数据的可靠传输。发送方如何知道发出的数据,接收方已经收到。

2、接收方的流量控制。因为各种原因,接收方可能来不及处理发送方发送的数据,而造成没有及时回应发送方,造成发送方不断的重发数据,最后造成接收方的主机宕机。

3、计算机网络的拥塞控制。数据在计算机网络之上传输,当出现数据拥塞时如何进行处理。

协议特点

  • 面向连接的传输协议

  • 应用程序在使用TCP之前,必须先建立TCP传输连接
    
  • 仅支持单播传输

  • TCP传输连接只能有两个端点,这里的端点指套接字,也就是IP地址和端口号的组合。不支持广播和组播。
    
  • 数据的可靠传输(可靠交付)

  • 现实生活中,我们在打电话的时候,当我们自己根对方说了一句话或者一段话之后,我们都会等待对方的回应,譬如她们会回答"哦"、"嗯"、"知道了",这时我们就会知道对方已经听到我们自己刚才说的话,如果她们不给予回应则会以为她们没有在听我讲话,也就是没有收到我发送的消息。
      
    TCP可靠传输的实现正是基于这样的例子,对于发送方发送的数据,接收方在接受到数据之后必须要给予确认,确认它收到了数据。如果在规定时间内,没有给予确认则意味着接收方没有接受到数据,然后发送方对数据进行重发。
      
    TCP的可靠传输是通过确认和超时重传的机制来实现的,而确认和超时重传的具体的实现是通过以字节为单位的滑动窗口机制来完成
    
  • 传输单位为数据段

  • 协议不限制数据传输大小,但是数据段大小受应用层传送的报文大小和所途经网络中的MTU值决定。MSS:最大数据段大小,最小数据段可能仅有21字节,其中20字节头部,1字节数据。
    
  • 支持全双工传输

  • 允许通讯双方的应用程序在任何时候都能发送数据。所以断开链接的时候需要双方都发起关闭,不然就会产生半关闭链接
    
  • TCP连接是基于字节流,而非报文流

  • TCP发送的数据是无界限的,因而在接收的时候需要根据长度来确认数据接收完成。
    
  • 每次发送的TCP数据段大小和数据段数都是可变的

  • 需要根据对方给出的窗口大小和当前网络的拥塞程度来决定。
      
    数据段大小的两个决定因素:
      
    1.每个TCP数据段的大小必须符合IP数据包的65515字节的有效载荷大小限制要求。
      
    2.每个网络都有一个MTU值,因此每个TCP数据段必须符合MTU限制要求。
    
  • 有流量控制、拥塞控制、快重传、快恢复机制

2、IP(Internet Protocol: 网络连接协议)

IP的责任就是把数据从源传送到目的地。它不负责保证传送可靠性,流控制,包顺序和其它对于主机到主机协议来说很普通的服务。

  • IP实现两个基本功能:寻址和分段。

  • IP使用四个关键技术提供服务:服务类型,生存时间,选项和报头校验码。

3、TCP/IP 的分层结构

OSI (Open System Interconnect, 开放系统互连参考模型)为开放式互连信息系统提供了一种理论上的网络模型,而 TCP/IP 则是实际实现运行的网络模型。TCP/IP 采用四层结构,它与 OSI 七层结构的对应关系如下图所示:

img

如上图所示,TCP/IP 各层的功能和协议情况可以简述为:

  1. 网络接口层(Host-to-Net Layer),对应 OSI 七层模型的下两层,负责实际数据的传输。主要协议包括:Ethernet、FDDI、PPP、SLIP和其他能传输 IP 数据包的任何协议。
  2. 网际层(Inter-network Layer),对应 OSI 七层模型的第三层,负责网络间的寻址和数据传输。主要协议:IP、ARP、RARP、ICMP、IGMP。
  3. 传输层(Transport Layer),对应 OSI 七层模型的第四层,负责提供可靠的传输服务。主要协议包括:TCP、UDP。
  4. 应用层(Application Layer),对应 OSI 七层模型的上面三层,负责实现一切与应用程序相关的功能。主要协议包括:FTP、HTTP、DNS、SMTP、NFS等。

4、数据封装与分用

对于我们介绍的 TCP/IP 协议,可以通过《TCP-IP详解》 的另一种协议分层方式来一览几个重要协议之间的关系。如下图所示:

img

对于数据的下行,我们简单称为 “封装”,反之称为 “分用”。

封装

应用程序使用 TCP/IP 协议传输应用数据时候,数据要被送入协议栈经过逐层封装,最终作为比特流在媒体上传送出去。其过程示意图如下所示:

img

注:从上图可以看到以太网帧的数据长度是有大小限制的,这个最大值称为 MTU,所以当 IP 数据包长度大于 MTU 时会被拆成多个帧传输,称为 “IP分片”。

协议栈中的每一层都需要向传递到该层的数据添加相应的协议头。 UDP 与 TCP 数据结构基本一致,唯一的不同是 UDP 传给 IP 的信息单元称作 UDP 数据报(UDP datagram),而且 UDP 的首部长为 8 字节。许多应用程序都会使用 TCP/UDP 协议,所以需要在报文的首部区分应用程序,这个区分标识就是 16bit 的“端口”。

ICMP、IGMP、TCP、UDP 都要使用 IP,所以 IP 数据包首部有一个 8bit 的标识字段专门用于区分,1表示为 ICMP, 2表示为 IGMP, 6表示为 TCP , 17表示为 UDP。

IP、ARP、RARP 都要使用以太网帧,所以帧结构中也有一个标识字段用于标识上层协议。

TCP、IP、以太网帧的数据结构将在后面介绍。

分用

当数据被媒体送达网络接口层时,会执行与 “封装” 相反的拆包过程,每层协议都要去检查报文首部中的协议标识,已确定接受数据的上层协议,这个过程就是 “分用”。其示意图如下所示:

img

5、以太网帧结构

img

实际上还有另外一种帧格式:IEEE 802.2/802.3,是在 RFC 1042 中定义的。与在 RFC 894 定义的以太网帧格式稍微有些不同,但是在 TCP/IP 的世界里后者更常见。

简单提一句,ARP/RARP 是用于在 32bit 的 IP 地址和 48bit 的 MAC 地址之间进行映射。具体协议内容请查阅相关资料。

6、IP 数据包结构

img

注意一下

上面描述的首部,不包括选项字段的 IP 头部长度为 20 字节长度,最高位在左边,记为 0bit。最低位在右边,记为 31bit。采用 “大端” 字节序进行传输,也就是对于 4 字节的 32bit 数据,从高位字节(0bit)开始传输 0~7,8~15,15~23,24~31bit。各字段的含义如下:

  • 4 位版本号,指协议版本号,值为4代表 IPv4。
  • 4 位首部长度,每一个计量单位是 32bit(4 byte),指的是包括选项字段在内的 IP 首部长度,由于是 4bit,所以 IP 首部最长只能是 60 字节(15 * 4)。
  • 8 位服务类型(TOS),包括 3bit 优先权字段(现在已经不用了),4bit TOS字段, 1bit 备用位。4bit TOS位分别代表:最小时延、最大吞吐量、最高可靠性和最小费用,只能设置其中 1bit,如果所有 4bit 均为0,那么就表示是一般服务。
  • 16 位 IP 数据包长度,计量单位 byte,包括首部和数据部分。能表示的最大长度为 65535,且这个字段是必须的,当 IP 数据包小于 46 字节时在以太网帧中数据将会被填充到 46 字节,这时候如果没有这个字段我们接收到帧后便不能得到正确的 IP 数据包。
  • 16 位标识字段,是数据包的唯一标识,通常主机每发送一个数据包就会 +1 ,在分片时会被复制到每一个分片中。
  • 3 位标志字段和 13 位(片)偏移字段,用于数据包分片和重组。3 位标志字段,0bit 保留;1bit 为 DF :0表示可以分片,1表示不能分片;2bit 为 MF:0表示最后一个分片,1表示还有分片。13 位(片)偏移字段,指示了这个分片在所属数据包中的位置,分片偏移以 8byte 做为计量单位,第一个分片偏移为 0。
  • 8 位生存时间(TTL),设置了数据包可以经过的最多路由器数量。
  • 8 位协议字段,前面已经提过了,标识上层协议的字段。
  • 16 位首部校验和,根据 IP 首部计算的检验和码,它不对首部后面的数据进行计算。采用的是 16bit 二进制反码求和,具体算法请查阅资料。
  • 32 位源 IP 地址和32位目标 IP 地址。
  • 选项字段,可变长的数据信息,具体选项定义请查询相关文档。尤其注意的是,选项必须以 32bit 作为计量单位,不满 32bit 需要填充 0。

7、TCP 数据段结构

img

不计算选项字段,TCP 首部的长度为 20byte。

  • 16 位源端口与目标端口号,用于标识发送端应用程序和接收端应用程序。
  • 32 位序号,无符号数,用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节,简单的可理解为对发送的数据(这个数据不一定是指数据字段的数据,比如建立连接时 SYN 字段设置为 1,也会消耗一个计数)按 byte 进行循环计数。
  • 32 位确认序号,无符号数,用于表示期望收到的下一个序号,ACK=1 时有效。
  • 4 位置首部长度,计量单位为 32bit,同 IP 首部长度字段。
  • URG,紧急指针有效。
  • ACK,确认序号有效。
  • PSH,接收方应该尽快将这个报文段交给应用层。
  • RST,重建连接。
  • SYN,同步序号用来发起一个连接。
  • FIN,发送端完成发送,用来结束一个连接。
  • 16 位窗口字段,这个与 TCP 的滑动窗口流量控制有关。
  • 16 位校验和,覆盖了整个的 TCP 报文段,包括首部和数据。与 UDP 数据报一样,TCP 数据报段在计算校验和时也包括一个 12 字节长的伪首部
  • 16 位紧急指针,这是一个正向偏移值,和序号字段中的值相加表示紧急数据最后一个字节的序号。
  • 选项字段,最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 SYN 标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。

img

12 字节长的 TCP/UDP 伪首部

注:TCP 数据报段伪首部起到双重校验的作用:1、通过伪首部的 IP 地址检验,TCP 可以确认 IP 没有接受不是发给本机的数据报;2、通过伪首部的协议字段检验,TCP 可以确认 IP 没有把应该传给其他高层协议(比如UDP、ICMP或者IGMP)的数据报传给 TCP 。

2、TCP/IP 协议 详解

1、流量控制

现实生活中,我们去一些热门的景点或者游乐园的某个娱乐项目时,都会需要进行排队,如果是小长假,则会出现人山人海的场景,这时这些机构就会控制每一次参观该景点的人数。

网络应用程序也是如此,当数据到达主机之后,TCP会将该数据放入相应的队列(又称为缓冲区)(如果让你自己基于UDP实现一个TCP模块供自己的应用程序使用,你也会采用这种方式),等待监听该端口的应用程序从队列中获取数据,应用程序一次所能处理的数据有限,因此不可能一次性取出队列中的所有数据,当队列已经满了,则无法再存放新的数据,只能将接受到的数据丢弃,因此TCP协议需要提供流量控制的能力,控制发送方每次发送数据的大小。

2、拥塞控制

现实生活中,高速公路也会堵车,在一段高速公路上,每辆车都在以很快的速度在运行,彼此并没有慢下来,但是为什么还是会出现堵车呢?通常都是因为每段道路的承载能力不一样,譬如当一段8车道公路上的汽车行驶到4车道公路上时,在这两段道路交汇的地方就会出现堵车。

计算机网络是由无数的数据链路组成的,每一段链路的承载能力不一样,也会出现数据拥堵的情况,这通常是由路由器和交换机的处理能力不同造成的。我们还需要知道,这种情况下的拥塞是不能避免的,因为我们无法要求所有链路的承载能力一样,因此我们只能对拥塞进行控制。TCP协议对拥塞控制也提出了响应的解决方案,这也是为什么TCP叫做传输控制协议而不叫做可靠传输协议的原因吧,同时也解释了为什么在计算机网络可靠性能大大提供的今天,TCP还继续发挥着其作用的原因。

简单来说 拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。

拥塞控制的原理:

发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。

3、慢启动机制

慢启动通过逐步增大拥塞窗口的值来控制网络拥塞。
慢启动的作用就是最大限度使用网络资源。

慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢开始算法,实时拥塞窗口大小是以字节为单位的。

发送方每收到一个确认就把窗口cwnd 加 1

当然收到单个确认但此确认多个数据报的时候就加相应的数值。所以一次传输轮次之后拥塞窗口就加倍。这就是乘法增长,和后面的拥塞避免算法的加法增长比较。

为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:

当cwnd<ssthresh时,使用慢开始算法。

当cwnd>ssthresh时,改用拥塞避免算法。

当cwnd=ssthresh时,慢开始与拥塞避免算法任意。

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。

img

再次提醒这里只是为了讨论方便而将拥塞窗口大小的单位改为数据报的个数,实际上应当是字节。

4、快重传和快恢复

 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

img

快重传配合使用的还有快恢复算法,有以下两个要点:

①当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把拥塞最大门限减半。但是接下去并不执行慢开始算法。

②考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为拥塞最大门限的大小,然后执行拥塞避免算法。

img

4、TCP连接管理

TCP 三次握手

客户端与服务端建立一个 TCP 连接共计需要发送 3 个包才能完成,这个过程称为三次握手(Three-way Handshake)。如上面所述,数据段的序号、确认序号以及滑动窗口大小都在这个过程中完成。socket 编程中,客户端执行 connect() 时,将触发三次握手。

img

如上图所示完成 TCP 连接的建立,客户端与服务端共计发送了 3 个报文段:

1、报文段1:客户端发送一个 SYN 报文段(握手信号)指明客户打算连接的服务器的端口,以及ISN(Initial Sequence Number 初始序号,这个例子中 ISN=1415531521)。ISN 的实现目前会随着时间的变化而变化,所以每次建立连接时的 ISN 都不同。这一步告诉服务器,我要访问你了。报文的数据字节数为 0,WIN:4096 表示发送端通告的窗口大小为 4096,上图中由于没有交换任何数据所以窗口维持在 4096。< MSS 1024> 表示由发端指明的最大报文段长度选项为 1024。

2、报文段2:服务器发回包含服务器的 ISN (初始序号)的 SYN 报文段(这个例子中 ISN=1823083521)作为应答。同时,将确认序号设置为客户的 ISN + 1 以报文段 1 进行确认。一个 SYN 将占用一个序号。服务器告诉客户端,我收到了你的访问请求。

3、报文段3:客户必须将确认序号设置为服务器的 ISN+1(1823083522) 以对服务器的 SYN 报文段进行确认,客户端又告诉服务器,我收到了你的确认。

4、连接建立,开始进行数据通信。

5、SYN:同步序列编号,TCP连接的第一个包,非常小的一种数据包

6、ACK:传输类控制字符,确认字符

为什么要使用三次握手

为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。比如:

一端(client)A发出去的第一个连接请求报文并没有丢失,而是因为某些未知的原因在某个网络节点上发生滞留,导致延迟到连接释放以后的某个时间才到达另一端(server)B。本来这是一个早已失效的报文段,但是B收到此失效的报文之后,会误认为是A再次发出的一个新的连接请求,于是B端就向A又发出确认报文,表示同意建立连接。如果不采用“三次握手”,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源

问题的本质是,信道是不可靠的,但是我们要建立可靠的连接发送可靠的数据,也就是数据传输是需要可靠的。在这个时候三次握手是一个理论上的最小值,并不是说是tcp协议要求的,而是为了满足在不可靠的信道上传输可靠的数据所要求的。

三次握手牵扯到的状态转换

LISTEN 表示socket已经处于listen状态了,可以建立连接

SYN_SENT 表示socket在发出connect连接的时候,会首先发送SYN报文,然后等待另一端发送的确认报文(ACK),表示这端已经发送完SYN报文了

SYN_RCVD 表示一端已经接收到SYN报文了

ESTABLISHED 表示已经建立连接了,可以发送数据了

超时重传机制

(1) 如果第一个包,A发送给B请求建立连接的报文(SYN)如果丢掉了,A会周期性的超时重传,直到B发出确认(SYN+ACK);
(2) 如果第二个包,B发送给A的确认报文(SYN+ACK)如果丢掉了,B会周期性的超时重传,直到A发出确认(ACK);
(3) 如果第三个包,A发送给B的确认报文(ACK)如果丢掉了,

A在发送完确认报文之后,单方面会进入ESTABLISHED的状态,B还是SYN_RCVD状态
如果此时双方都没有数据需要发送,B会周期性的超时发送(SYN+ACK),直到收到A的确认报文(ACK),此时B也进入ESTABLISHED状态,双方可以发送数据;
如果A有数据发送,A发送的是(ACK+DATA),B会在收到这个数据包的时候自动切换到ESTABLISHED状态,并接受数据(DATA);
如果这个时候B要发送数据,B是发送不了数据的,会周期性的超时重传(SYN+ACK)直到收到A的确认(ACK)B才能发送数据。

同时连接

上面所示的是一方主动连接另外一方的情况,实际上 TCP 也允许双方同时主动连接,这种情况下就要求连接双方提前知道对方的端口。实际中很少出现这种需求。这种情况下,要发送 4 个报文段才能建立起连接。

img

附:SYN 洪水攻击

由三次握手可以看出,当服务器收到 SYN 数据报文段后将为连接分配资源,如果服务器没有收到 ACK 报文段就会造成半开连接,浪费服务器资源。SYN 洪水攻击就是利用 TCP 的这个缺陷,通过向服务器发送海量的 SYN 报文段而耗尽服务器资源。一般有两种方式:1、客户端恶意不发送 ACK;2、在发送给服务器的 SYN 报文段中提供虚假的 IP 地址,造成服务器永远收不到 ACK。

这种攻击手段对于现代网络效果不大,但并不能完全防范。一般较新的 TCP/IP 协议栈实现提都供了防范手段,主要手段包括 SYN cookie 、 SynAttackProtect 保护机制、增加最大半连接、缩短超时时间和限定某一段时间内来自同一来源请求新连接的数量等。

这里有一片不错的文章介绍如何防御 SYN 洪水攻击:TCP洪水攻击(SYN Flood)的诊断和处理

TCP 四次挥手

建立一个连接需要 3 次握手,而终止一个连接要经过 4 次握手。这由 TCP 的半关闭(half-close,连接的一端在结束它的发送后还能接收来自另一端数据的能力。具体的请查阅 TCP 半关闭的相关资料)造成的。 TCP 连接是全双工,因此每个方向必须单独地进行关闭。也就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接,当一端收到一个 FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。

连接双方都可发起这个操作,socket 编程中,任何一方执行 close() 触发挥手操作。

img

上图的 4 次挥手示意图是接着上面 3 次握手进行的,假设没有应用数据传输,所以报文段4的序号紧接着报文段1的序号(ACK 的发送是没有任何代价的,不会消耗序号)。图中所示的是一方主动关闭(首先发送 FIN 数据报),另一方被动关闭,实际上 TCP 也允许双方同时主动关闭。

同时关闭

img

5、拓展

为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?

- 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一 个报文里来发送。
- 但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未 必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文 和FIN报文多数情况下都是分开发送的。 

为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED 状态(就好比从SYN_SENT 状态到ESTABLISH 状态那样),但是我们必须假想网络是不可靠的,你无法保证你(客户端)最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK 状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT 状态的作用就是用来重发可能丢失的ACK报文。

关闭TCP连接一定需要4次挥手吗?

- 不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源)
- 我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的

TCP的有限状态机

状态 描述
CLOSED 呈阻塞,关闭状态,表示当前主机没有活动的传输连接或没有正在进行传输连接
LISTEN 呈监听状态,表示服务器正在等待新的传输连接进入
SYNRCVD 表示服务器已经收到一个传输连接请求,但尚未确认
SYNSENT 表示客户端已经发出一个传输连接请求,等待服务器的确认
ESTABLISHED 传输连接建立
FIN_WAIT_1 主动关闭方的主机已经发送关闭连接请求,等待对方确认
CLOSE_WAIT 被动关闭方的主机收到主动关闭方的关闭连接请求,并已确认
FIN_WAIT_2 主动关闭方的主机已经收到对方对主动关闭连接请求的确认,等待对方发送关闭传输连接请求
LAST_ACT 被动关闭方的主机已经发送关闭连接请求,等到主动方确认
TIME_WAIT 主动关闭方的主机收到对方发送的关闭连接请求

二、UDP协议

1、UDP(User Datagram Protocol:用户数据报协议)

UDP(User Datagram Protocol:用户数据报协议)

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议,提供面向事务的简单不可靠信息传送服务。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

UDP是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去。

1、UDP是无连接的

即发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。

2、尽最大努力交付

只管发送,不确认对方是否接收到,尽最大努力交付,不保证可靠交付,因此主机不需要维持复杂的链接状态表(这里面有许多参数)。

3、面向报文

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。每个数据报的大小限制在64K之内,速度快。

4、支持一对一、一对多、多对一和多对多的交互通信

5、首部开销小

只有8个字节,比TCP 的20个字节的首部要短

6、没有拥塞控制系统、没有超时重发、所以速度快

7、当应用程序使用广播或多播时只能使用UDP协议

应用场景:多媒体教室/网络流媒体 / 视频实时共享

2、UDP 和 TCP 的区别

特征点 TCP UDP
是否连接 面向连接 面向非连接
传输可靠性 可靠 会丢包,不可靠
应用场景 传输数据量大 传输量小
速度

1、TCP协议是提供面向连接的、可靠的字节流服务;传输数据量大,传输速度相对较慢;

2、UDP是提供面向事务的简单不可靠信息传送服务;传输数据量小,传输速度相对较快

三、Port 和 Socket了解

1、Port(端口)

伴随着传输层诞生的概念。它可以将网络层的IP通信分送到各个通信通道。UDP协议和TCP协议尽管在工作方式上有很大的不同,但它们都建立了从一个端口到另一个端口的通信。

2、Socket (套接字)

Socket的作用:提供网络通信的能力

Socket是什么

Socket是对TCP/IP协议的封装,它的出现只是使得程序员更方便地使用TCP/IP协议栈而已。Socket本身并不是协议,它是应用层与TCP/IP协议族通信的中间软件抽象层(在上图的TCP、IP四层网络模型中,Socket就是介于 应用层和传输层中间的抽象层。),是一组调用接口(TCP/IP网络的API函数)。

TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。 
这个就像操作系统会提供标准的编程接口,比如win32编程接口一样。 

TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口

Socket的实现原理

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。一个socket句柄(文件描述符)代表两个地址对:本地ip:port -- 远程:port

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

Socket之间是如何通信

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

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

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

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

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

iOS中的Socket编程

sys/socket中的用法

客户端:

#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

@implementation ViewController {
    int _clientSocket;  //nc -lk 1024
    IBOutlet UITextField *mytext;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //建立连接
    _clientSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1024);
    addr.sin_addr.s_addr = inet_addr(@"192.168.0.116".UTF8String);
    int connectResult = connect(_clientSocket, (const struct sockaddr *)&addr, sizeof(addr));
    if (connectResult == 0) {
        mytext.text = @"连接成功";
    } else {
        mytext.text = @"连接失败";
    }
}

- (IBAction)send:(id)sender {
	//发送消息和监听消息
    dispatch_queue_t queue = dispatch_queue_create("并发", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        const char *str = @"我是小白".UTF8String;
        ssize_t sendLen = send(_clientSocket, str, strlen(str), 0);
        char *buf[1024];
        ssize_t recvLen = recv(_clientSocket, buf, sizeof(buf), 0);
        NSString *recvStr = [[NSString alloc]initWithBytes:buf length:recvLen encoding:NSUTF8StringEncoding];
        dispatch_async(dispatch_get_main_queue(), ^{
            mytext.text = recvStr;
        });
    });
}
@end

//服务器

#import "ViewController.h"
#import <arpa/inet.h>
#import <netinet/in.h>
#import <sys/socket.h>

@interface ViewController ()
//监听到的客户端ip地址
@property (weak, nonatomic) IBOutlet UILabel *client_ip;
//监听到的客户端端口
@property (weak, nonatomic) IBOutlet UILabel *client_port;
//服务器手动发送消息
@property (weak, nonatomic) IBOutlet UITextField *server_sendMSG;
//显示客户端发来的消息
@property (weak, nonatomic) IBOutlet UITextView *client_showMSG;
//连接状态
@property (nonatomic, weak) IBOutlet UILabel * status;
//监听按钮点击
@property (weak, nonatomic) IBOutlet UIButton *connectBtn;
//记录按钮状态
@property (nonatomic,assign) int flag;
@end

@implementation ViewController
{
    int _serverSocket;
    int _clientSocket;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //开启服务
    [self startServer];
}
- (void)startServer{
    //按钮监听是否启动服务
    [self.connectBtn addTarget:self action:@selector(connectBtnEvent:) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark - 建立监听
- (void)connectAndlistenPort:(int)port{
    _serverSocket=socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    //如果返回值不为-1,则成功
    if(_serverSocket != -1){
        NSLog(@"socket success");
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));//清零操作
        addr.sin_len=sizeof(addr);
        addr.sin_family=AF_INET;
        addr.sin_port=htons(port);
        addr.sin_addr.s_addr=INADDR_ANY;
        //绑定地址和端口号
        int bindAddr = bind(_serverSocket, (const struct sockaddr *)&addr, sizeof(addr));
        //开始监听
        if (bindAddr == 0) {
            NSLog(@"bind(绑定) success");
            int startListen = listen(_serverSocket, 5);//5为等待连接数目
            if(startListen == 0){
                NSLog(@"listen success");
                //回到主线程更新UI
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.status.text = @"监听成功";
                });
            }else{
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.status.text = @"监听失败";
                });
            }
        }
    }
}

#pragma mark - 阻塞直到客户端连接
- (void)accept{
    struct sockaddr_in peeraddr;
    socklen_t addrLen;
    addrLen=sizeof(peeraddr);
    NSLog(@"prepare accept");
    //接受到客户端clientSocket连接,获取到地址和端口
    int clientSocket=accept(_serverSocket, (struct sockaddr *)&peeraddr, &addrLen);
    _clientSocket = clientSocket;
    if (clientSocket != -1) {
        NSLog(@"accept success,remote address:%s,port:%d",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
        //回到主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.client_ip.text =[NSString stringWithUTF8String:inet_ntoa(peeraddr.sin_addr)];
            self.client_port.text = [NSString stringWithFormat:@"%d",ntohs(peeraddr.sin_port)];
        });
        char buf[1024];
        size_t len=sizeof(buf);
        //接受到客户端消息
        recv(clientSocket, buf, len, 0);
        NSString* str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
        //主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.client_showMSG.text = str;
            NSLog(@"%@",str);
        });
    }
}

#pragma mark - 关闭socket
- (void)connectBtnEvent:(UIButton *)sender {
    if (self.flag==0) {
        [self.connectBtn setTitle:@"断开连接" forState:UIControlStateNormal];
        dispatch_queue_t SERIAL_QUEUE =  dispatch_queue_create("SERIAL", DISPATCH_QUEUE_SERIAL);
        dispatch_async(SERIAL_QUEUE, ^{
            self.flag=1;
            [self connectAndlistenPort:1024];
            while (self.flag) {
                //扫描客户端连接
                [self accept];
            }
        });
    }else{
        [self.connectBtn setTitle:@"启动服务器" forState:UIControlStateNormal];
        self.status.text = @"监听失败";
        shutdown(_clientSocket, SHUT_RDWR);
        shutdown(_serverSocket, SHUT_RDWR);
        close(_clientSocket);
        close(_serverSocket);
        self.flag=0;
    }
}
#pragma mark - 发送消息

- (IBAction)sendBtn:(UIButton *)sender {
    [self sentAndRecv:_clientSocket msg:_server_sendMSG.text];
}

//发送数据并等待返回数据
- (void)sentAndRecv:(int)clientSocket msg:(NSString *)msg {
    dispatch_queue_t q_con =  dispatch_queue_create("CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(q_con, ^{
        const char *str = msg.UTF8String;
        send(clientSocket, str, strlen(str), 0);
        char *buf[1024];
        ssize_t recvLen = recv(clientSocket, buf, sizeof(buf), 0);
        NSString *recvStr = [[NSString alloc] initWithBytes:buf length:recvLen encoding:NSUTF8StringEncoding];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.client_showMSG.text = recvStr;
        });
    });
}
@end

GCDAsynSocket

//客户端,具体使用百度一下
@implementation ViewController {
    
    IBOutlet UITextField *mytext;
    GCDAsyncSocket *clientSocket;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    clientSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
 
    NSError * error = nil;
    BOOL result = [clientSocket connectToHost:@"192.168.0.116" onPort:1024 error:&error];
    
    if (result) {
        mytext.text = @"连接成功";
    } else {
        mytext.text = @"连接失败";
    }
    [clientSocket readDataWithTimeout:- 1 tag:0];
}

- (IBAction)send:(id)sender {
    
    NSData *data = [@"测试数据" dataUsingEncoding:NSUTF8StringEncoding];
 
    [clientSocket writeData:data withTimeout: -1 tag:0];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    mytext.text = text;
}

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    mytext.text = [NSString stringWithFormat:@"服务器IP: %@-------端口: %d", host,port];
    [clientSocket readDataWithTimeout:- 1 tag:0];
}

@end

很详细

https://blog.csdn.net/rock_joker/article/details/76769404