0%

Linux 网络编程

摘要

  • 学习并整理Linux系统中关于网络编程的知识。
    • 一方面用于巩固知识,更加深入理解网络编程,建立良好的体系结构;
    • 另一方面用于当前的面试,学到的东西能够说出来。

网络编程基础概念

  • 协议(protocol): 通信双方必须遵守地规矩,由ISO(组织)规定,RPC(标准)文档
  • 现存地网络中主要用的是TCP/IP模型
  • TCP/IP模型为四层
    • 应用层:http(超文本传输协议),ftp(文件传输协议),telnet(远程登录),ssh(安全外壳协议),stmp(简单邮件发送),pop3(收邮件)
    • 传输层:tcp(传输控制协议),udp(用户数据包协议)
    • 网络层:ip(网际互连协议),icmp(网络控制消息协议),igmp(网络组管理协议)
    • 网络接口层:arp(地址转换协议),rarp(反向地址转换协议),mpls(多协议标签交换)

传输控制协议 – TCP

  • 传输控制协议(Transmission Control Protocol, TCP)是一种面向连接地,可靠地,基于字节流地传输层通信协议,由IETF地RFC 793定义。

  • 在简化地计算机网络OSI模型中,它完成第四层传输层所指定地功能。用户数据报协议(UDP)是同一层内另一个重要的传输协议。

  • 在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下地中间层。不同主机地应用层之间经常需要可靠地,像管道一样地连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

  • 应用层向TCP层发送用于网间传输的,用8位字节表示的数据流,然后TCP把数据流分割成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。

  • 之后TCP把结果包传给IP层,由它来透过网络将包传送给接收端实体的TCP层。

  • TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。

  • 然后接收端实体对已成功收到的包发回一个相应的确认信息(ACK);

  • 如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失并进行重传。

  • TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和。

  • 数据在TCP层称为流(Stream),数据分组称为分段(Segment)。

  • 作为比较,数据在IP层称为Datagram,数据分组称为分片(Fragment)。

  • UDP中分组称为 Message

运作方式

  • TCP协议的运行可划分为三个阶段:
    • 连接建立(connection establishment)
    • 数据传送(data transfer)
    • 连接终止(connection termination)
  • 操作系统将TCP连接抽象为套接字表示的本地端点(local end-point),作为编程接口给程序使用。在TCP连接的声明期内,本地端点要经历一系列的状态改变。
    传输控制协议状态图

建立通路

  • TCP用三次握手(或称三路握手, three-way handshake)过程建立一个连接。在连接建立过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。

  • 一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端(服务器端)打开一个套接字(socket)然后监听来自另一方(客户端)的连接,这就是通常所指的被动打开(passive open)。服务器端被动打开以后,客户端就能开始建立主动打开(active open)
    TCP连接的正常建立

  • 服务端执行了 listen() 后,就在服务器上建立起两个队列

    • SYN队列:存放完成了二次握手的结果。队列长度由 listen() 的参数 backlog 指定。
    • ACCEPT队列:存放完成了三次握手的结果。队列长度由 listen() 的参数 backlog 指定。
  • 三次握手协议的过程:

    • 客户端(通过执行connect函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数A作为消息序列号。
    • 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身携带一个随机产生的序号B。
    • 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为A+1,而ACK的确认码则为B+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求帧从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新建立一个新的用于双向通信的sockfd,并返回。
  • 如果服务器端接到了客户端发的SYN后回了SYN-ACK后客户端掉线了,服务器端没有收到客户端回来的ACK,那么,这个连接处于一个中间状态,既没成功,也没失败。于是,服务器端如果在一定时间内没有收到的TCP会重发SYN-ACK。

  • 在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接。使用三个TCP参数来调整行为:tcp_synack_retries 减少重试次数;tcp_max_syn_backlog,增大SYN连接数;tcp_abort_on_overflow决定超出能力时的行为。

  • “三次握手”的目的是:

    • 为了防止已失效的连接(connect)请求报文段传送到了服务端,因而产生错误”,也即为了解决“网络中存在延迟的重复分组”问题。
    • 例如:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
    • 采用“三次握手”的办法可以防止上述现象发生,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。

资源使用

  • 主机收到一个TCP包时,用两端的IP地址与端口号来标识这个TCP包属于哪个session。使用一张表来存储所有的session,表中的每条称作Transmission Control Block(TCB),tcb结构的定义包括连接使用的源端口、目的端口、目的ip、序号、应答序号、对方窗口大小、己方窗口大小、tcp状态、tcp输入/输出队列、应用层输出队列、tcp的重传有关变量等。
  • 服务器端的连接数量是无限的,只受内存的限制。客户端的连接数量,过去由于在发送第一个SYN到服务器之前需要先分配一个随机空闲的端口,这限制了客户端IP地址的对外发出连接的数量上限。
  • 从Linux 4.2开始,有了socket选项IP_BIND_ADDRESS_NO_PORT,它通知Linux内核不保留usingbind使用端口号为0时内部使用的临时端口(ephemeral port),在connect时会自动选择端口以组成独一无二的四元组(同一个客户端端口可用于连接不同的服务器套接字;同一个服务器端口可用于接受不同客户端套接字的连接)
  • 对于不能确认的包、接收但还没读取的数据,都会占用操作系统的资源。

数据传输

  • 在TCP的数据传送状态,很多重要的机制保证了TCP的可靠性和强壮性。它们包括:
    • 使用序号,对收到的TCP报文段进行排序以及检测重复的数据;
    • 使用校验和检测报文段的错误,即无错传输;
    • 使用确认和计时器来检测和纠正丢包或延时;
    • 流控制(Flow control);
    • 拥塞控制(Congestion control);
    • 丢失包的重传。

终结通路

  • 连接终止使用了四路握手过程(或称四次握手,four-way handshake),在这个过程中连接的每一侧都独立地被终止。当一个端点要停止它这一侧的连接,就向对侧发送FIN,对侧回复ACK表示确认。因此,拆掉一侧的连接过程需要一对FIN和ACK,分别由两侧端点发出。
    TCP连接的正常终止
  • 首先发出FIN的一侧,如果给对侧的FIN响应了ACK,那么就会超时等待2*MSL时间,然后关闭连接。在这段超时等待时间内,本地的端口不能被新连接使用;避免延时的包的到达与随后的新连接相混淆。RFC793定义了MSL为2分钟,Linux设置成了30s。参数tcp_max_tw_buckets控制并发的TIME_WAIT的数量,默认值是180000,如果超限,那么,系统会把多的TIME_WAIT状态的连接给destory掉,然后在日志里打一个警告(如:time wait bucket table overflow)
  • 连接可以工作在TCP半开状态。即一侧关闭了连接,不再发送数据;但另一侧没有关闭连接,仍可以发送数据。已关闭的一侧仍然应接收数据,直至对侧也关闭了连接。
  • 也可以通过测三次握手关闭连接。主机A发出FIN,主机B回复FIN & ACK,然后主机A回复ACK.
    TCP_CLOSE

端口

  • TCP使用了通信端口(Port number)的概念来标识发送方和接收方的应用层。对每个TCP连接的一端都有一个相关的16位的无符号端口号分配给它们。
  • 端口被分为三类:众所周知的、注册的和动态/私有的。
    • 众所周知的端口号是由因特网赋号管理局(IANA)来分配的,并且通常被用于系统一级或根进程。众所周知的应用程序作为服务器程序来运行,并被动地侦听经常使用这些端口的连接。例如:FTP、TELNET、SMTP、HTTP、IMAP、POP3等。
    • 注册的端口号通常被用来作为终端用户连接服务器时短暂地使用的源端口号,但它们也可以用来标识已被第三方注册了的、被命名的服务。
    • 动态/私有的端口号在任何特定的TCP连接外不具有任何意义。可能的、被正式承认的端口号有65535个。

应用

  • TCP并不是对所有的应用都适合,一些新的带有一些内在的脆弱性的运输层协议也被设计出来。比如,实时应用并不需要甚至无法忍受TCP的可靠传输机制。
  • 在这种类型的应用中,通常允许一些丢包、出错或拥塞,而不是去校正它们。例如通常不使用TCP的应用有:流媒体、网络游戏、IP电话(VoIP)等等。任何不是很需要可靠性或者是想将功能减到最少的应用可以避免使用TCP。在很多情况下,当只需要多路复用应用服务时,用户数据报协议(UDP)可以代替TCP为应用提供服务。
  • 除外,由于TCP的实现是由操作系统提供,而TCP的悠久历史、系统级别的配置机制,一些特性在特定的网络环境下会成为一种累赘而且无法优化,所以也有一些通过在UDP上重新实现用户层级的类似TCP的面向连接的、可靠的、基于字节流的类传输层协议,来代替TCP,例如基于UDP的数据传输协议、QUIC。

用户数据报协议 UDP

  • 用户数据报协议(User Datagram Protocol, UDP,又称用户数据包协议)是一个简单的面向数据包的通信协议,位于OSI模型的传输层。该协议由David P. Reed在1980年设计且在RFC 768中被规范。典型网络上的众多使用UDP协议的关键应用在一定程度上是相似的。
  • 在TCP/IP模型中,UDP为网络层以上和应用层以下提供了一个简单的接口。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份(所以UDP有时候也被认为是不可靠的数据包协议)。UDP在IP数据包的头部仅仅加入了复用和数据校验字段
  • UDP适用于不需要或在程序中执行错误检查和纠正的应用,它避免了协议栈中此类处理的开销。对时间有较高要求的应用程序通常使用UDP,因为丢弃数据包比等待或重传导致延迟更可取

可靠性

  • 由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。某些应用程序(如TFTP)可能会根据需要在应用程序层中添加基本的可靠性机制。
  • 一些应用程序不太需要可靠性机制,甚至可能因为引入可靠性机制而降低性能,所以它们使用UDP这种缺乏可靠性的协议。流媒体,实时多人游戏和IP语音(VoIP)是经常使用UDP的应用程序。 在这些特定应用中,丢包通常不是重大问题。如果应用程序需要高度可靠性,则可以使用诸如TCP之类的协议。
  • 例如,在VoIP中延迟和抖动是主要问题。如果使用TCP,那么任何数据包丢失或错误都将导致抖动,因为TCP在请求及重传丢失数据时不向应用程序提供后续数据。如果使用TCP,那么应用程序则需要提供必要的握手,例如实时确认已收到的消息。
  • 由于UDP缺乏拥塞控制,所以需要基于网络的机制来减少因失控和高速UDP流量负荷而导致的拥塞崩溃效应。换句话说,因为UDP发送端无法检测拥塞,所以像使用包队列和丢弃技术的路由器之类的网络基础设备会被用于降低UDP过大流量。数据拥塞控制协议(DCCP)设计成通过在诸如流媒体类型的高速率UDP流中增加主机拥塞控制,来减小这个潜在的问题

应用

  • 许多关键的互联网应用程序使用UDP,包括:
    • 域名系统(DNS),其中查询阶段必须快速,并且只包含单个请求,后跟单个回复数据包
    • 动态主机配置协议(DHCP),用于动态分配IP地址;
    • 简单网络管理协议(SNMP);
    • 路由信息协议(RIP);
    • 网络时间协议(NTP)。
  • 流媒体、在线游戏流量通常使用UDP传输。 实时视频流和音频流应用程序旨在处理偶尔丢失、错误的数据包,因此只会发生质量轻微下降,而避免了重传数据包带来的高延迟。
  • 由于TCP和UDP都在同一网络上运行,因此一些企业发现来自这些实时应用程序的UDP流量影响了使用TCP的应用程序的性能,例如销售、会计和数据库系统。 当TCP检测到数据包丢失时,它将限制其数据速率使用率。由于实时和业务应用程序对企业都很重要,因此一些人认为开发服务质量解决方案至关重要
  • 某些VPN应用(如OpenVPN)使用UDP并可以在应用程序级别实现可靠连接和错误检查。

Socket 基本概念与基本流程

  • Socket起源于Unix,Unix/Linux基本哲学之一就是一切皆文件(普通文件,目录,硬件设备,进程,管道都是文件),Socket也可以被认为是文件,所以也可以对Socket使用文件I/O地相关操作,可以用 打开(open)->读写(read/write)->关闭(close) 模式来进行操作。

  • socket一词的起源

    • 在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”
  • Linux平台下,socket() 返回地值被称为文件描述符fd(File Descriptor),用来唯一标识一个套接字,在Windows平台它称为句柄(handle)。

  • 套接字的主流程很简单,在服务端下:

    • 使用 socket() 创建套接字
    • 使用 bind() 分配IP地址和端口号
    • 使用 listen() 将套接字转换为可受连接状态,开始监听前面分配的IP和端口号
    • 使用 accept() 受理连接请求
    • 使用 write()/read() 来和客户端交换数据
    • 使用 close() 关闭连接。
  • 在客户端下:

    • 不需要 bind() 分配IP和端口号,而是由 connect() 自动分配端口号加上自身的IP
    • 使用 write()/read() 和服务端交换数据
    • 使用 close() 关闭连接
      C++Socket套接字基本流程
  • Windows平台和Linux的差别较小,在创建套接字之前需要调用 WSAStartUp(),关闭连接使用 closesocket(),关闭连接之后使用 WSACleanUp() 注销,其他没有差别了,所以在项目中可以很方便地通过 WIN32 宏来判断环境编写跨平台代码。
    C++Windows和Linux网络编程基本流程

  • socket是对TCP/IP协议的封装,它本身并不是协议,而是一个调用接口(API),通过它才能使用TCP/IP协议

  • socket是对TCP/IP协议族的一种封装,是应用层与TCP/IP协议族通信的中间软件抽象层。

网络字节序与主机字节序

  • 主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
    • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
    • Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
  • 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
  • 所以
    • 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。

socket的基本操作

  • 既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

socket()函数

  • 使用 socket() 函数创建一个套接字
    1
    2
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
  • socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作
  • 正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
    • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
    • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
  • 注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
  • 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口

bind() 函数

  • 正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

    1
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同

    • ipv4对应的是:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      struct sockaddr_in {
      sa_family_t sin_family; /* address family: AF_INET */
      in_port_t sin_port; /* port in network byte order */
      struct in_addr sin_addr; /* internet address */
      };

      /* Internet address. */
      struct in_addr {
      uint32_t s_addr; /* address in network byte order */
      };
    • ipv6对应的是:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      struct sockaddr_in6 { 
      sa_family_t sin6_family; /* AF_INET6 */
      in_port_t sin6_port; /* port number */
      uint32_t sin6_flowinfo; /* IPv6 flow information */
      struct in6_addr sin6_addr; /* IPv6 address */
      uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
      };

      struct in6_addr {
      unsigned char s6_addr[16]; /* IPv6 address */
      };
    • 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 */
      };
  • addrlen:对应的是地址的长度。

  • 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。

  • 这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

listen(), connect() 函数

  • 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,
  • 如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
    1
    2
    int listen(int sockfd, int backlog);
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
  • connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

accept() 函数

  • TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
  • TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。
  • TCP服务器监听到这个请求之后,就会调用accept()函数去接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
    1
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • accept函数的第一个参数为服务器的socket描述字;第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址;第三个参数为协议地址的长度。
  • 如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
  • 注意
    • accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字
    • 一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。
    • 内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

read(), write() 等函数

  • 至此服务器与客户端已经建立好连接了,可以调用网络I/O进行读写操作,即实现了网络中不同进程之间的通信。
  • 网络I/O操作有下面几组
    • read()/write()
    • recv()/send()
    • readv()/writev()
    • recvmsg()/sendmsg()
    • recvfrom()/sendto()
  • 推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <unistd.h>

    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);

    #include <sys/types.h>
    #include <sys/socket.h>

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    struct sockaddr *src_addr, socklen_t *addrlen);

    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
  • write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。
  • 在网络程序中,当我们向套接字文件描述符写时有俩种可能。
    • write的返回值大于0,表示写了部分或者是全部的数据
    • 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。
      • 如果错误为EINTR表示在写的时候出现了中断错误。
      • 如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

close() 函数

  • 在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
    1
    2
    3
    #include <unistd.h>

    int close(int fd);
  • close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
  • 注意
    • close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

socket中TCP的三次握手建立连接详解

  • 我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
    • 客户端向服务器发送一个SYN J
    • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
    • 客户端再想服务器发一个确认ACK K+1
  • 只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
    socket中发送的TCP三次握手
  • 从图中可以看出,当客户端调用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返回,至此三次握手完毕,连接建立。
  • 总结
    • 客户端的connect在三次握手的第二次返回,而服务器端的accept在三次握手的第三次返回。

socket中TCP的四次握手释放连接详解

  • 上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
    socket中发送的TCP四次握手
  • 图示过程如下:
    • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
    • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
    • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
    • 接收到这个FIN的源发送端TCP对它进行确认。
  • 这样每个方向上都有一个FIN和ACK。
感谢老板支持!敬礼(^^ゞ