Socket 网络套接字

1、Socket 套接字

  • 所谓 Socket,通常称为 “套接字”,网络应用程序通过套接字向网络发送请求或者应答网络请求。Socket 通常用于描述 IP 地址和端口,是应⽤层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,是一个通信链的句柄,可以用来实现不同虚拟机或者不同计算机之间的通信。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接⼝后面。

  • Socket 起源于 Unix,而 Unix/Linux 基本哲学之一就是 “一切皆文件”,都可以用 “打开 open –> 读写 write/read –> 关闭 close” 模式来操作。我的理解就是 Socket 就是该模式的一个实现,Socket 即是一种特殊的文件,一些 Socket 函数就是对其进行的操作(读/写 IO、打开、关闭)。

  • Socket 一词的起源。在组网领域的首次使用是在 1970 年 2 月 12 日发布的文献 IETF RFC33 中发现的,撰写者为 Stephen Carr、Steve Crocker 和 Vint Cerf。根据美国计算机历史博物馆的记载,Croker 写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比 BSD 的套接字接口定义早了大约 12 年。”

  • Socket 的英文原义是 “孔” 或 “插座”。作为 BSD UNIX 的进程通信机制,取后一种意思。通常也称作 “套接字”,用于描述 IP 地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在 Internet 上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个 Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket 正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供 110 伏交流电,有的则提供有线电视节目。客户软件将插头插到不同编号的插座,就可以得到不同的服务。

  • Socket 就是为网络服务提供的一种机制。

  • 在 Unix中,网络即是 Socket,并不局限在 TCP/UDP。
  • Socket 可以用于自定义协议。
  • 通信的两端都是 Socket。
  • 网络通信其实就是 Socket 间的通信。
  • 数据在两个 Socket 间通过 IO 传输。
  • Socket 开始是纯 C 语言的,是跨平台的。

  • 应用场景:

    • 1、即时通讯

      • 特点:实时性,感觉不到延时和掉线,因为会监听 socket 的状态,如果掉线会进行重连。
    • 2、服务器推送(web 与服务器通信)

      • 客户端与服务器建立一个 TCP 连接,实现全双工通信(核心:客户端定时向服务器发送心跳数据包)。

1.1 网络

  • 网络中进程之间如何通信

    • 本地的进程间通信(IPC)有很多种方式,但可以总结为下面 4 类:

      • 消息传递(管道、FIFO、消息队列)
      • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
      • 共享内存(匿名的和具名的)
      • 远程过程调用(Solaris 门和 Sun RPC)
    • 网络中进程之间如何通信,首要解决的问题是如何唯一标识一个进程,否则通信无从谈起。在本地可以通过进程 PID 来唯一标识一个进程,但是在网络中这是行不通的。其实 TCP/IP 协议族已经帮我们解决了这个问题,网络层的 “ip 地址” 可以唯一标识网络中的主机,而传输层的 “协议 + 端口” 可以唯一标识主机中的应用程序(进程)。这样利用三元组(协议,ip 地址,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    • 使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD 的套接字(socket)和 UNIX System V 的 TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在。

  • 1、传输协议(通讯的规则):

    • 1) TCP:传输控制协议:

      • 建立连接,形成传输数据的通道(建立连接的三次握手,断开连接的四次握手)。
      • 在连接中进行大数据传输,数据大小不受限制。
      • 通过三次握手完成连接,是可靠协议,数据安全送达。
      • 必须建立连接,效率会稍低。
    • 2) UDP:用户数据报协议:

      • 只管发送,不确认对方是否接收到。
      • 不需要建立连接,将数据及源和目的封装成数据包中,每个数据报的大小限制在 64K 之内。
      • 因为无需连接,因此是不可靠协议。
      • 不需要建立连接,速度快。
      • 应用场景:多媒体教室/网络流媒体。
    • 3) 常见网络协议:

      1
      2
      3
      4
      5
      6
      7
      8
        应用层协议       |     端口       |        说明
      -----------------|----------------|----------------------------
      HTTP | 80 | 超文本传输协议
      HTTPS | 443 | HTTP+SSL,HTTP 的安全版
      FTP | 20, 21, 990 | 文件传输
      POP3 | 110 | 邮局协议
      SMTP | 25 | 简单邮件传输协议
      telnet | 23 | 远程终端协议
  • 2、IP 地址(主机名):

    • 网络中设备的唯一标示。不易记忆,可以用主机名(域名)。

    • 1) IP V4:

      • 0~255.0~255.0~255.0~255 ,共有 2\^8\^4 = 2\^32 = 42 亿。
    • 2) 本地回环地址:

      • 每台机器都有自己的本地回环地址,IP 为 127.0.0.1 ,主机名为 localhost。如果 127.0.0.1 ping 不通,则网卡不正常。

      • 本地 hosts 文件修改,终端:

        1
        2
        3
        4
        5
        6
        7
        8
        $ cd /etc
        $ sudo vim hosts
        $ 输入密码进入 hosts 文件编辑界面
        $ 将光标移动到指定位置

        英文输入模式下按 i 键进入编辑状态,
        英文输入模式下按 esc 键进入命令状态,
        在命令状态下输入 :wq 回车,保存退出 hosts 文件。
  • 3、端口号:

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

    • 1) Netcat 的使用:

      • Netcat 是 Mac 终端下用于调试和检查网络的工具包,可用于创建 TCP/IP 连接。
      • 终端:$ nc -lk 12345,开启监听,终端将始终监听本地计算机 12345 端口的数据。
  • 4、网络参考模型:

ISO 参考模型 TCP/IP 参考 说明
应用层 应用层 FTP,HTTP,TELNET,SMTP,DNS 等协议
表示层
会话层
传输层 传输层 Socket 开发,TCP 协议,UDP 协议
网络层 网络互连层 路由器,IP 协议,ICMP 协议,ARP 协议,RARP 协议和 BOOTP 协议
数据链路层 网络接口层 交换机
物理层 网线

1.2 Socket 通讯过程

  • 1、TCP 通信

    • TCP 通信面向连接,可靠传输(保证数据正确性,顺序性),用于传输大量数据(流模式)、速度慢,建立连接开销比较大(时间,系统资源)。

    • 流模式:在连接持续的过程中,基本上都是从同一个主机发出的,因此,需要保证数据是有序的到达。

    • 三次握手(建立 TCP 连接,需要 C 和 S 发送三个包),四次挥手(TCP 连接的断开需要发送 4 个包)。

    • TCP 通信流程图

      • 1)服务器端先初始化 Socket,然后与端⼝绑定(bind),对端⼝进⾏监听(listen)。调用 accept 阻塞,等待客户端连接;
      • 2)在这时如果有个客户端初始化⼀个 Socket,然后连接服务器(connect);
      • 3)如果连接成功,这时客户端与服务器端的连接就建⽴了。
    • 根据连接启动的方式以及本地套接字要连接的目标,Socket 之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。

      • 1) 服务器监听:

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

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

        • 是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
  • 2、UDP 通信

    • UDP 通信非连接,不可靠传输,速度快,用于传输少量数据。

    • 只要知道接收端的 IP 和端口,任何主机都可以向接收端发送数据。

    • UDP 通信流程图

      ![][icon2]
      
  • 3、TCP 的三次握手建立连接

    • socket 中的三次握手建立连接的过程,如下图:

    • 图示过程如下:

      • 当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;
      • 服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K,ACK J+1,这时 accept 进入阻塞状态;
      • 客户端收到服务器的SYN K,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;
      • 服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。
  • 4、TCP 的四次握手释放连接

    • socket 中的四次握手释放连接的过程,如下图:

    • 图示过程如下:

      • 某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;
      • 另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认。它的接收也作为文件结束符传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据;
      • 一段时间之后,接收到文件结束符的应用进程调用 close 关闭它的 socket。这导致它的 TCP 也发送一个 FIN N;
      • 接收到这个 FIN 的源发送端 TCP 对它进行确认。这样每个方向上都有一个 FIN 和 ACK。

1.3 Socket 编程框架

  • iOS 中 Socekt 编程框架:

    • BSDSocket(纯 C):

      • 一套 unix 系统下的 socket API。

      • iOS 系统基于 unix,所以支持底层的 BSD Socket,在 Xcode 中可以直接使用。

    • CFSocket(纯 C):

      • 苹果对对底层 BSD Socket 进行轻量级的封装。

      • 主要使用的 API:CFSocekt 用于建立连接,CFStream 用于读写数据。

    • CFNetwork(纯 C):

      • 基于 OS 层 BSDSocket 封装,用于网络通信,早期的网络请求框架 ASIHTTPRequest 就是基于 CFNetwork 进行的封装。

      • 主要使用的 API:CFSocket 用于底层的通信,CFStream 用于数据的读写。

    • CocoaAsyncSocket(OC):

      • 目前比较常用。

      • 基于 CFSocket、GCD 进行的封装。

      • 支持 TCP 和 UDP。

      • 完整的回调函数(用于处理各种回调事件,连接成功,断开连接,收到数据等)。

      • 需要注意的问题:

        • 1、Socekt 连接成功回调方法中主动调用:[self.socket readDataWithTimeout:-1 tag:0];,相当于主动添加一个读取请求,不然不会执行读取信息回调方法。
        • 2、读取信息回调方法中,读取完信息后,主动调用一下 [self.socket readDataWithTimeout:-1 tag:0];,读取完信息后,重新向队列中添加一个读取请求,不然当收到信息后不会执行读取回调方法。
    • WebSocket(OC):

      • 适用于 web 应用的可持久连接的全双工通讯协议,被称为 “Web 的 TCP”,实现了浏览器和服务器的双向通信同样也适用于原生应用,协议本身使用 “ws://URL” 格式,是在标准 http 协议之上实现的,浏览器和服务器之间只需做一次握手操作后,就会创建一个快速通信通道。

      • 解决问题:以前的服务器推送是通过浏览器轮询的方式进行,时间间隔太长:不实时,体验差,太短:消耗资源,服务器负载太大。

    • SocketRocket(OC):

      • Square 公司封装的一个 WebSocket 框架,用于原生和 web APP 和服务器通信。
    • SocketIO(OC):

      • 也支持 WebSocket,内部封装了 SocektRocket。

1.4 Socket 常用函数

  • BSDSocket

    • 1) 创建:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      函数原型:
      int socket(int domain, int type, int protocol);

      参数说明:
      domain:协议域,又称协议族(family),协议族决定了 socket 的地址类型,在通信中必须采用对应的地址

      常用的协议族:
      AF_INET (ipv4):用 ipv4 地址(32 位的)与端口号(16 位的)的组合
      AF_INET6(ipv6):用 ipv6 地址(128 位的)与端口号(16 位的)的组合
      AF_LOCAL(或称 AF_UNIX,Unix 域 Socket):用一个绝对路径名作为地址
      AF_ROUTE :

      type:指定 Socket 类型

      常用的 socket 类型:
      SOCK_STREAM(流式/TCP) :是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用
      SOCK_DGRAM (数据报式/UDP):是一种无连接的 Socket,对应于无连接的 UDP 服务应用
      SOCK_RAW :
      SOCK_PACKET :
      SOCK_SEQPACKET :

      protocol:指定协议

      常用协议:
      IPPROTO_TCP(TCP 传输协议) :
      IPPROTO_UDP(UDP 传输协议) :
      IPPROTO_STCP(STCP 传输协议):
      IPPROTO_TIPC(TIPC 传输协议):

      注意:1. type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当第三个参数为 0 时,
      会自动选择第二个参数类型对应的默认协议。
      2. Windows Socket 下 protocol 参数中不存在 IPPROTO_STCP。

      返回值:
      如果调用成功就返回新创建的套接字的描述符(大于 0),如果失败就返回 INVALID_SOCKET(Linux 下失败返回 -1)。
      • 套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
    • 2) 绑定:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);

      参数说明:
      sockfd :套接字描述符
      addr :是一个 sockaddr 结构指针,该结构中包含了要结合的地址和端口号
      addrlen:addr 的长度

      返回值:
      如果函数执行成功,返回值为 0,否则为 -1
      • 当我们调用 socket() 创建一个 socket 时,返回的 socket 描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind() 函数,否则就当调用 connect()、listen() 时系统会自动随机分配一个端口。

      • 通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip 地址 + 端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的 ip 地址组合。这就是为什么通常服务器端在 listen 之前会调用 bind(),而客户端就不会调用,而是在 connect() 时由系统随机生成一个。

    • 3) 监听:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      函数原型:
      int listen(int sockfd, int backlog);

      参数说明:
      sockfd :套接字描述符
      backlog:socket 可以排队的最大连接个数

      返回值:
      如果函数执行成功,返回值为 0,否则为 -1
      • socket() 函数创建的 socket 默认是一个主动类型的,listen() 函数将 socket 变为被动类型的,等待客户的连接请求。
    • 4) 接受连接:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

      参数说明:
      sockfd :监听套接字描述符
      addr :返回客户端的地址
      addrlen:addr 的长度

      返回值:
      成功返回由内核自动生成的一个全新的描述符(即连接成功的客户端的套接字描述符),失败返回 -1
      • TCP 服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket()、connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数去接收请求,这样连接就建立好了。之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。

      • accept() 的第一个参数为服务器的 socket 描述字,是服务器开始调用 socket() 函数生成的,称为监听 socket 描述符;而 accept() 函数返回的是已连接的 socket 描述字。一个服务器通常通常仅仅只创建一个监听 socket 描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接 socket 描述符,当服务器完成了对某个客户的服务,相应的已连接 socket 描述符就被关闭。

    • 5) 连接:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);

      参数说明:
      sockfd :套接字描述符
      addr :指定数据发送的目的地,也就是服务器端的地址
      addrlen:addr 的长度

      返回值:
      如果函数执行成功,返回值为 0,否则为 -1
    • 6) 断开连接:

      1
      2
      3
      4
      5
      6
      7
      8
      函数原型:
      int close(int sockfd);

      参数说明:
      sockfd:套接字描述符

      返回值:
      成功返回客户端的文件描述符,失败返回 -1
      • close 一个 TCP socket 的缺省行为是把该 socket 标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为 read 或 write 的第一个参数。

      • 注意:close 操作只是使相应 socket 描述字的引用计数 -1,只有当引用计数为 0 的时候,才会触发 TCP 客户端向服务器发送终止连接请求。

    • 7) 发送数据:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      函数原型:
      ssize_t write(int sockfd, const void * buf, size_t size);

      参数说明:
      sockfd:套接字描述符
      buf :发送内容地址,message.UTF8String 将字符串转换成 UTF8 的 ASCII 码,一个汉字需要 3 个字节
      size :发送内容长度,是字节的个数,需使用 strlen() 计算所有字节的长度

      返回值:
      如果成功,则返回发送的字节数,失败则返回 -1,并设置 errno 变量。
      如果错误为 EINTR 表示在写的时候出现了中断错误。如果为 EPIPE 表示网络连接出现了问题(对方已经关闭了连接)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      函数原型:
      ssize_t send(int sockfd, const void * buf, size_t size, int flags);

      参数说明:
      sockfd:套接字描述符
      buf :发送内容地址,message.UTF8String 将字符串转换成 UTF8 的 ASCII 码,一个汉字需要 3 个字节
      size :发送内容长度,是字节的个数,需使用 strlen() 计算所有字节的长度
      flags :发送方式标志,一般为 0

      返回值:
      如果成功,则返回发送的字节数,失败则返回 -1
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      函数原型:
      ssize_t sendto(int sockfd, const void * buf, size_t size, int flags, const struct sockaddr * dest_addr, socklen_t addrlen);

      参数说明:
      sockfd :套接字描述符
      buf :待发送数据的缓冲区
      size :缓冲区长度,是字节的个数,需使用 strlen() 计算所有字节的长度
      flags :调用方式标志位, 一般为 0, 改变 Flags,将会改变 Sendto 发送的形式
      dest_addr:可选指针,指向目的套接字的地址
      addrlen :dest_addr 的长度

      返回值:
      如果成功,则返回发送的字节数,失败则返回 -1
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      ssize_t sendmsg(int sockfd, const struct msghdr * msg, int flags);

      参数说明:
      sockfd:套接字描述符
      msg :送数据的内容
      flags :调用方式标志位

      返回值:
      如果成功,则返回发送的字节数,失败则返回 -1
    • 8) 接收数据:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      函数原型:
      ssize_t read(int sockfd, void * buf, size_t size);

      参数说明:
      sockfd:套接字描述符
      buf :用于接收数据的缓冲区
      size :缓冲区长度

      返回值:
      如果成功,返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。
      如果错误为 EINTR 说明读是由中断引起的,如果是 ECONNREST 表示网络连接出了问题。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      函数原型:
      ssize_t recv(int sockfd, void * buf, size_t size, int flags);

      参数说明:
      sockfd:套接字描述符
      buf :用于接收数据的缓冲区
      size :缓冲区长度
      flags :指定调用方式
      取值:
      MSG_PEEK:查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;
      MSG_OOB :指示接收到 out-of-band 数据(即需要优先处理的数据)。

      返回值:
      如果成功,返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      函数原型:
      ssize_t recvfrom(int sockfd, void * buf, size_t size, int flags, struct sockaddr * src_addr, socklen_t * addrlen);

      参数说明:
      sockfd :套接字描述符
      buf :接收数据缓冲区
      size :缓冲区长度
      flags :调用操作方式。是以下一个或者多个标志的组合体,可通过 or 操作连在一起:
      MSG_DONTWAIT:操作不会被阻塞
      MSG_ERRQUEUE:指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,
      使用者应该提供足够大的缓冲区。导致错误的原封包通过 msg_iovec 作为一般的数据来传递。导致错误
      的数据报原目标地址作为 msg_name 被提供。错误以 sock_extended_err 结构形态被使用
      MSG_PEEK :指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据
      MSG_TRUNC :返回封包的实际长度,即使它比所提供的缓冲区更长, 只对 packet 套接字有效
      MSG_WAITALL :要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被
      接收的数据类型不同,仍会返回少于请求量的数据
      MSG_EOR :指示记录的结束,返回的数据完成一个记录
      MSG_CTRUNC :指明由于缓冲区空间不足,一些控制数据已被丢弃
      MSG_OOB :指示接收到 out-of-band 数据(即需要优先处理的数据)
      MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据
      src_addr:可选指针,指向装有源地址的缓冲区
      addrlen :可选指针,指向 address 缓冲区长度值

      返回值:
      如果成功,返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      ssize_t recvmsg(int sockfd, struct msghdr * msg, int flags);

      参数说明:
      sockfd:套接字描述符
      buf :用于接收数据的缓冲区
      size :缓冲区长度

      返回值:
      如果成功,返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。
    • 9) 获取本地协议地址:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      int getsockname(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

      参数说明:
      sockfd :套接字描述符
      addr :返回本地协议的地址
      addrlen:addr 的长度

      返回值:
      成功返回 0,失败返回 1
    • 10) 获取远程协议地址:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      函数原型:
      int getpeername(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

      参数说明:
      sockfd :套接字描述符
      addr :返回远程协议的地址
      addrlen:addr 的长度

      返回值:
      成功返回 0,失败返回 1
      • getsockname 和 getpeername 调度时机很重要,如果调用时机不对,则无法正确获得地址和端口。

        • TCP:

          • 对于服务器来说,在 bind 以后就可以调用 getsockname 来获取本地地址和端口,虽然这没有什么太多的意义。getpeername 只有在连接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。

          • 对于客户端来说,在调用 socket 时候内核还不会分配 IP 和端口,此时调用 getsockname 不会获得正确的端口和地址(链接没建立更不可能调用 getpeername),当然如果调用了 bind 以后可以使用 getsockname。想要正确得到对方地址(一般客户端不需要这个功能),则必须在连接建立以后,同样连接建立以后,此时客户端地址和端口就已经被指定,此时是调用 getsockname 的时机。

        • UDP

          • 没有连接的 UDP 不能调用 getpeername,但是可以调用 getsockname,和 TCP 一样,他的地址和端口不是在调用 socket 就指定了,而是在第一次调用 sendto 函数以后。
            • 已经连接的 UDP,在调用 connect 以后,这 2 个函数都是可以用的(同样,getpeername 也没太大意义。如果你不知道对方的地址和端口,不可能会调用 connect)。
    • 11) 获取套接字选项当前值:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      函数原型:
      int getsockopt(int sockfd, int level, int optname, void * optval, socklen_t * optlen);

      参数说明:
      sockfd :套接字描述符
      level :选项定义的层次。例如,支持的层次有 SOL_SOCKET、IPPROTO_TCP。、
      optname:需获取的套接字选项
      optval :指向存放所获得选项值的缓冲区
      optlen :指向 optval 缓冲区的长度值

      返回值:
      成功返回 0

1.5 IP 地址表示

  • 1、IPv4 地址用一个 32 位整数来表示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct sockaddr_in {
    __uint8_t sin_len; // 地址长度
    sa_family_t sin_family; // IP 地址协议族,必须设为 AF_INET
    in_port_t sin_port; // 通信端口
    struct in_addr sin_addr; // 以网络字节排序的 4 字节 IPv4 地址
    char sin_zero[8]; // 填充项,是为了让 sockaddr 与 sockaddr_in 两个数据结构保持大小相同而保留的空字节
    };

    struct sockaddr {
    __uint8_t sa_len; // 地址长度
    sa_family_t sa_family; // IP 地址协议族,必须设为 AF_INET
    char sa_data[14]; // 地址值
    };

    struct in_addr {
    uint32_t s_addr; // 按照网络字节顺序存储 IP 地址
    };

    sockaddr_in 和 sockaddr 是并列的结构,指向 sockaddr_in 的结构体的指针也可以指向 sockaddr 的结构体,并代替它。
    也就是说,你可以使用 sockaddr_in 建立你所需要的信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    struct sockaddr_in address;

    // 清空内存
    memset(&address, 0, sizeof(address)); // 清空指向的内存中的存储内容,因为分配的内存是随机的,
    // 如果不清空可能会因为垃圾数据产生不必要的麻烦
    bzero(& address, sizeof(address)); // 清空指向的内存中的存储内容

    // 设置地址值
    ser_addr.sin_len = sizeof(address); // 地址长度
    address.sin_family = AF_INET; // 协议族
    address.sin_port = htons(12345); // 端口数据高位在前低位在后 12345 => 3039 => 3930
    address.sin_addr.s_addr = inet_addr("192.168.88.100"); // inet_addr 函数可以把 ip 地址转换成一个整数

    // 设置所有地址值
    address.sin_addr.s_addr = INADDR_ANY; // 指定地址为 0.0.0.0 的地址,这个地址事实上表示不确定地址,
    // 或所有地址、所有 ip、任意地址。一般来说,在各个系统中均定义成为 0 值

    // 获取地址值
    int ip_port = ntohs(address.sin_port); // 获取端口,如获取到:53746
    char *ip_addr = inet_ntoa(address.sin_addr); // 获取 IP 地址,如获取到:192.168.88.100

    // 获取本地地址
    socklen_t addrLen = sizeof(address);
    getsockname(self.clientSockfd, (struct sockaddr *)&address, &addrLen);
    int ip_port = ntohs(address.sin_port); // 本地端口
    char *ip_addr = inet_ntoa(address.sin_addr); // 本地 IP 地址
  • 2、IPv6 地址用一个 128 位整数来表示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct sockaddr_in6 {
    __uint8_t sin6_len; // length of this struct(sa_family_t) 地址长度
    sa_family_t sin6_family; // AF_INET6 (sa_family_t) IP 地址协议族族,必须为 AF_INET6
    in_port_t sin6_port; // Transport layer port # (in_port_t) 通信端口
    __uint32_t sin6_flowinfo; // IP6 flow information 标记连接通信量
    struct in6_addr sin6_addr; // IP6 address 以网络字节排序的 6 字节 IPv6 地址
    __uint32_t sin6_scope_id; // scope zone index 地址所在的接口索引(范围 ID)
    };

    struct in6_addr {
    union {
    __uint8_t __u6_addr8[16];
    __uint16_t __u6_addr16[8];
    __uint32_t __u6_addr32[4];
    } __u6_addr;
    };
  • 3、Unix 域对应的是:

    1
    2
    3
    4
    5
    6
    #define UNIX_PATH_MAX    108

    struct sockaddr_un {
    sa_family_t sun_family; // AF_UNIX
    char sun_path[UNIX_PATH_MAX]; // pathname
    };
  • 4、网络字节序与主机字节序:

    • 主机字节序:就是我们平常说的大端和小端模式:不同的 CPU 有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的 Big-Endian 和 Little-Endian 的定义如下:

      • a) Little-Endian:就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
      • b) Big-Endian :就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    • 网络字节序:4 个字节的 32 bit 值以下面的次序传输:首先是 0~7bit,其次 8~15bit,然后 16~23bit,最后是 24~31bit,这种传输次序称作大端字节序。由于 TCP/IP 首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

    • 所以在将一个地址绑定到 socket 的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是 Big-Endian。由于这个问题曾引发过血案,公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给 socket。

2、BSDSocket 的使用

3、CFSocket 的使用

4、AsyncSocket 的使用

5、WebSocket 的使用

6、CFNetwork 的使用

7、TCP 和 UDP 的区别

  • 1、TCP 是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但 TCP 的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;而 UDP 不是面向连接的,UDP 传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说 UDP 是无连接的、不可靠的一种数据传输协议。

  • 2、也正由于 1 所说的特点,使得 UDP 的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以 UDP 的实时性更好。知道了 TCP 和 UDP 的区别,就不难理解为何采用 TCP 传输协议的 MSN 比采用 UDP 的 QQ 传输文件慢了,但并不能说 QQ 的通信是不安全的,因为程序员可以手动对 UDP 的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP 因为在底层协议的封装上没有采用类似 TCP 的 “三次握手” 而实现了 TCP 所无法达到的传输效率。

8、http,TCP/IP 与 Socket 之间的区别

  • 1、TCP/IP 连接

    • 手机能够使用联网功能是因为手机底层实现了 TCP/IP 协议,可以使手机终端通过无线网络建立 TCP 连接。TCP 协议可以对上层网络提供接口,使上层网络数据的传输建立在 “无差别” 的网络之上。建立起一个 TCP 连接需要经过“三次握手”:

      • 第一次握手:客户端发送 syn 包(syn=j) 到服务器,并进入 SYN_SEND 状态,等待服务器确认;
      • 第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
      • 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。
    • 握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过 “四次握手”,服务器和客户端交互,最终确定断开。

  • 2、HTTP 连接

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

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

  • 3、Socket 连接

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

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

  • 4、Socket 连接与 TCP/IP 连接

    • 创建 Socket 连接时,可以指定使用的传输层协议,Socket 可以支持不同的传输层协议(TCP 或 UDP),当使用 TCP 协议进行连接时,该 Socket 连接就是一个 TCP 连接。
  • 5、Socket 连接与 HTTP 连接

    • 很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是 Socket 连接,服务器就可以直接将数据传送给客户端。若双方建立的是 HTTP 连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在 “询问” 服务器是否有新的数据,如果有就将数据传给客户端。
  • 6、Socket 与 http 协议

    • 一个是发动机(Socket),提供了网络通信的能力 ,一个是轿车(Http),提供了具体的方式。两个计算机之间的交流无非是两个端口之间的数据通信,具体的数据会以什么样的形式展现是以不同的应用层协议来定义的。如 HTTP、FTP、…

    • http 协议是应用层的协义。

    • Socket 是对端口通信开发的工具,它要更底层一些。

  • 7、网络请求方式 get 和 post

    • 这是 http 协议的两种方法,另外还有 head, delete 等,这两种方法有本质的区别,get 只有一个流,参数附加在 url 后,大小个数有严格限制且只能是字符串。post 的参数是通过另外的流传递的,不通过 url,所以可以很大,也可以传递二进制数据,如文件的上传。
  • 8、TCP/IP、Http、Socket 的区别

    • IP 协议对应于网络层,TCP 协议对应于传输层,HTTP 协议对应于应用层,而我们平时说的最多的 socket 是什么呢,实际上 socket 是对 TCP/IP 协议的封装,Socket 本身并不是协议,而是一个调用接口(API)。通过 Socket,我们才能使用 TCP/IP 协议。实际上,Socket 跟 TCP/IP 协议没有必然的联系。Socket 编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket 的出现只是使得程序员更方便地使用 TCP/IP 协议栈而已,是对 TCP/IP 协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如 create、listen、connect、accept、send、read 和 write 等等。
文章目录
  1. 1. 1、Socket 套接字
    1. 1.1. 1.1 网络
    2. 1.2. 1.2 Socket 通讯过程
    3. 1.3. 1.3 Socket 编程框架
    4. 1.4. 1.4 Socket 常用函数
    5. 1.5. 1.5 IP 地址表示
  2. 2. 2、BSDSocket 的使用
  3. 3. 3、CFSocket 的使用
  4. 4. 4、AsyncSocket 的使用
  5. 5. 5、WebSocket 的使用
  6. 6. 6、CFNetwork 的使用
  7. 7. 7、TCP 和 UDP 的区别
  8. 8. 8、http,TCP/IP 与 Socket 之间的区别
隐藏目录