本文最后更新于 2025-06-25,学习久了要注意休息哟

第三章 TCP通讯

TCP 概述

TCP 向应用层提供可靠的面向连接的数据流传输服务。它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达)。

通过源/目的 IP 可以唯一地区分网络中的两个设备,再通过源/目的端口可以唯一地区分网络中两个通信的应用程序。

3.1.1 TCP协议的特征

面向连接

TCP 是一种面向连接的协议,这意味着在数据交换之前,两个通信端必须先建立连接。该连接通过三次握手过程(SYN,SYN-ACK,ACK)来建立,确保双方都准备好数据交换。

可靠传输

TCP 通过序列号和确认应答机制来确保数据的可靠传输。每个报文段被分配一个序列号,接收方通过发送确认应答(ACK)来确认已收到特定序列号的报文段。如果发送方没有在合理的超时时间内收到确认应答,它将重传该报文段。

流量控制

TCP 使用窗口大小调整机制来进行流量控制,防止发送方过快地发送数据,导致接收方来不及处理。通过调整窗口大小,TCP 能够动态地管理数据的传输速率,避免网络拥塞和数据丢失。

拥塞控制

TCP 实现了多种拥塞控制算法(如慢启动、拥塞避免、快速重传和快速恢复)来避免网络中的过度拥塞。这些算法能够根据网络条件动态调整数据的发送速率,从而提高网络的整体效率和公平性。

数据排序

由于网络延迟和路由变化,TCP 报文段可能会乱序到达接收方。TCP 能够根据序列号重新排序这些乱序到达的报文段,确保数据以正确的顺序交付给应用层。

端到端通讯

TCP(传输控制协议)提供端到端的通信。每个 TCP 连接由四个关键元素唯一确定:

  1. 源 IP 地址
  2. 源端口号
  3. 目标 IP 地址
  4. 目标端口号

这种方式确保了数据能够在复杂的网络环境中准确地从一个端点传输到另一个端点。通过这四个要素,TCP 能够在网络上有效地管理多个并发连接,实现可靠的数据传输。

3.1.2 TCP 数据报格式 和 状态

TCP数据报格式

TCP 报文段各字段详细介绍

源端口(Source Port):16 位

  • 标识发送端应用程序的端口号。用于表明数据从哪个端口发出,接收方可以使用此信息将数据转发到对应的应用程序。

目的端口(Destination Port):16 位

  • 标识接收端应用程序的端口号。用于确定数据应传递到接收方的哪个应用程序或服务。

序号(Sequence Number):32 位

  • 标识数据包的序列号。在 TCP 连接中,发送的数据流会对每个字节进行编号,序号用于标记当前报文段的第一个字节的编号。它确保数据在接收方按正确的顺序重组,防止数据丢失或重复。

确认号(Acknowledgment Number):32 位

  • 用于确认接收到的数据包。它表示发送方希望下一个数据包的序号。若此字段非零,则表明接收方已经成功收到之前所有字节的数据。

TCP 头长(Header Length):4 位

  • 表明 TCP 头部长度的大小,以 32 位字(4 字节)为单位。最小长度为 20 字节(不包含选项字段),最大长度为 60 字节(包含选项字段)。

标志位(Flags):6 位

  • TCP 头部中有 6 个标志位,用于控制数据的传输及连接状态的管理:
  • URG(紧急指针有效位):当此位为 1 时,表示紧急指针有效,发送的数据需优先处理。
  • ACK(确认号有效位):当此位为 1 时,确认号字段有效;为 0 时,忽略确认号字段。
  • PSH(推送位):表示推送操作。发送方请求接收方立即将接收的数据传递给应用层,而不需等待缓冲区填满。
  • RST(复位位):用于复位连接。当检测到异常连接或者拒绝非法连接请求时,将该位设置为 1。
  • SYN(同步位):用于建立连接。在连接建立的初始阶段(握手阶段),该位为 1。
  • FIN(结束位):用于关闭连接。当发送方完成数据发送时,将该位设置为 1,表示不再有数据要发送。

窗口大小(Window Size):16 位

  • 表示接收方能够接收的最大数据量(字节数),用于流量控制。发送方根据该值确定在接收方确认之前可以发送的字节数。

校验和(Checksum):16 位

  • 用于检验 TCP 头部和数据字段的完整性。发送方计算并填写该字段,接收方收到数据后也计算并对比校验和,若发现不一致,说明数据被损坏,需要重传。

紧急指针(Urgent Pointer):16 位

  • URG 标志位为 1 时,此字段表示紧急数据在数据部分的结束位置。紧急数据通常需要接收方立即处理。

选项(Options):可变长度

  • 用于支持一些附加的 TCP 功能,如最大报文段大小(MSS)、时间戳、窗口缩放等。此字段的长度可变,以 4 字节为单位进行填充。

TCP状态

  1. CLOSED(关闭状态):

    • 无连接状态,这是初始状态,表示没有任何连接 活动。
  2. LISTEN(监听状态):

    • 服务器端等待来自远程 TCP 客户端的连接请求。服务器会在某个特定的端口上监听。
  3. SYN-SENT(同步已发送状态):

    • 客户端主动打开连接,并向服务器发送 SYN 报文段,进入该状态。此时等待服务器的 SYN-ACK 报文段以确认连接请求。
  4. SYN-RECEIVED(同步已接收状态):

    • 服务器接收到客户端的 SYN 报文段后,回复 SYN-ACK 报文段,并进入该状态,等待客户端的确认(ACK)。
  5. ESTABLISHED(已建立状态):

    • 表示连接已建立,可以进行数据传输。该状态是 TCP 状态机中最主要的状态,表示客户端和服务器之间的连接已经成功建立并可进行双向数据传输。
  6. FIN-WAIT-1(终止等待 1 状态):

    • 主动关闭连接的一方在发送 FIN 报文段后进入该状态,表示请求断开连接,等待对方的确认。
  7. FIN-WAIT-2(终止等待 2 状态):

    • 主动关闭连接的一方在收到对方的 ACK 报文段后进入此状态,等待对方发送 FIN 报文段。
  8. CLOSE-WAIT(关闭等待状态):

    • 被动关闭的一方接收到对方的 FIN 报文段后进入此状态,等待应用程序发出关闭请求。
  9. CLOSING(关闭状态):

    • 当双方几乎同时关闭连接,双方各自发送 FIN 报文段后进入此状态,等待对方的确认(ACK)。
  10. LAST-ACK(最后确认状态):

    • 被动关闭的一方在发送 FIN 报文段后进入此状态,等待对方的确认(ACK)。
  11. TIME-WAIT(时间等待状态):

    • 主动关闭连接的一方在发送最后一个 ACK 报文段后进入此状态,并保持一段时间(通常为 2 * 最大报文段生存时间,2MSL),以确保对方已接收到 ACK 报文段,防止旧的重复数据包影响新的连接。
  12. CLOSED(关闭状态):

    • 最终状态,表示连接完全关闭,没有任何连接活动。

3.2 三次握手 四次挥手

3.2.1 TCP 三次握手

说明

  • 用 小写的 seq 表示 TCP 的报文头部的序号

  • 用 小写的 ack 表示确认号

  • 用 大写的 ACK 表示 确认号 的 有效位

  • 未提到的标志位 均为 0 号

seq 和 ack 的关系

seq 和 ack 的关系

在建立连接之前,通信的双方需要进行三次握手,确保双方的连接是有效且安全的。
三次握手过程如下:

三次握手

步骤解释

第一次握手:客户端发送 SYN

  • 客户端处于 CLOSED 状态,准备与服务器建立连接。客户端发送一个带有 SYN(同步)标志的 TCP 报文段,并指定初始序列号 ISN(c)(Initial Sequence Number of client)。发送后,客户端进入 SYN-SENT 状态,等待服务器的响应。

  • 报文段内容:

    • SYNseq = ISN(c)(客户端的初始序列号)

第二次握手:服务器发送 SYN-ACK

  • 服务器在 LISTEN 状态下监听来自客户端的连接请求。当服务器收到客户端的 SYN 报文段后,它会发送一个 SYN-ACK(同步-确认)报文段,表示同意连接。服务器的报文段包含自己的初始序列号 ISN(s),以及对客户端的初始序列号的确认 ack = ISN(c) + 1

  • 服务器进入 SYN-RECEIVED 状态,等待客户端的确认。

  • 报文段内容:

    • SYN, ACKseq = ISN(s)(服务器的初始序列号),ack = ISN(c) + 1(确认客户端的序列号)

第三次握手:客户端发送 ACK

  • 客户端收到服务器的 SYN-ACK 报文段后,会发送一个 ACK(确认)报文段以确认收到服务器的 SYN 报文段。此报文段的 ack 字段为 ISN(s) + 1,表示确认了服务器的初始序列号。

  • 客户端进入 ESTABLISHED 状态,表示连接已建立。

  • 服务器收到客户端的 ACK 报文段后,也进入 ESTABLISHED 状态,双方完成连接的建立过程。

  • 报文段内容:

    • ACKseq = ISN(c) + 1(客户端序列号),ack = ISN(s) + 1(确认服务器的序列号)

三次握手的目的

三次握手的目的是:确保客户端和服务器双方都有能力发送和接收数据,协商初始序列号,并建立起一个可靠的连接通道。

第一次握手:客户端发送连接请求。

第二次握手:服务器确认请求并同意连接。

第三次握手:客户端确认连接建立,双方进入已建立状态(ESTABLISHED)。

三次握手完成后,客户端和服务器之间的 TCP 连接就正式建立,可以开始传输数据。

3.2.2 TCP 四次挥手

四次挥手

说明

U:
    U 是客户端(Client)在第一次挥手时发送 FIN 报文段的序列号(seq)。
    当客户端决定关闭连接时,发送一个 FIN 报文段,该报文段的序列号为 U,表示客户端希望关闭连接。
    服务器收到该 FIN 报文段后,将返回一个 ACK 报文段,其中的确认号(ack)为 U + 1,表示确认收到客户端的关闭请求。
V:
    V 是服务器(Server)在第二次挥手时的当前序列号(seq)。
    服务器收到客户端的 FIN 报文段后,发送一个 ACK 报文段作为响应,这个 ACK 报文段的序列号为 V,确认号为 U + 1。
    V 表示服务器的当前发送序列号。
W:
    W 是服务器在第三次挥手时发送 FIN 报文段的序列号(seq)。
    当服务器也准备关闭连接时,它会发送一个 FIN 报文段,该报文段的序列号为 W。
    客户端收到此 FIN 报文段后,将返回一个 ACK 报文段,其中的确认号(ack)为 W + 1,表示确认收到服务器的关闭请求。

步骤解释

第一次挥手:客户端发送 FIN

  • 当客户端(Client)准备关闭连接时,它会发送一个带有 FIN 标志的报文段给服务器(Server)。此时,客户端进入 FIN-WAIT-1 状态。
  • 报文段内容:
    • FIN, ACKseq = U(客户端的当前序列号),ack = V(确认服务器的序列号)

第二次挥手:服务器发送 ACK

  • 服务器收到客户端的 FIN 报文段后,确认已经接收到关闭连接的请求,发送一个 ACK 报文段作为响应。此时,服务器进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
  • 报文段内容:
    • ACKseq = V(服务器的当前序列号),ack = U + 1(确认客户端的序列号)

第三次挥手:服务器发送 FIN

  • 服务器在发送完 ACK 报文段后,如果确认没有再需要发送的数据,就会发送一个带有 FIN 标志的报文段,表示它也准备关闭连接。此时,服务器进入 LAST-ACK 状态。
  • 报文段内容:
    • FIN, ACKseq = W(服务器的当前序列号),ack = U + 1(确认客户端的序列号)

第四次挥手:客户端发送 ACK

  • 客户端收到服务器的 FIN 报文段后,发送最后一个 ACK 报文段,确认已接收服务器的关闭请求。此时,客户端进入 TIME-WAIT 状态,等待一段时间(通常为 2 * 最大报文段生存时间,即 2MSL),以确保服务器收到确认后再关闭连接。

  • 服务器在收到客户端的 ACK 后进入 CLOSED 状态,表示连接已完全关闭。

  • 报文段内容:

    • ACKseq = U + 1(客户端的当前序列号),ack = W + 1(确认服务器的序列号)

四次挥手的目的

  • 四次挥手的目的是:确保双方都能完全关闭连接,且所有未发送的数据都被可靠地传输和确认。
  • 第一次挥手:客户端发出关闭请求(FIN)。
  • 第二次挥手:服务器确认关闭请求(ACK)。
  • 第三次挥手:服务器发出关闭请求(FIN)。
  • 第四次挥手:客户端确认服务器的关闭请求(ACK)。

通过四次挥手,TCP 确保了双方安全可靠地关闭连接,不会因为连接关闭而造成数据丢失或错误。

3.3 TCP 数据传输

3.3.1 TCP 传输模拟

初始条件

  • 总数据量:500 字节
  • 每次发送的数据量:100 字节
  • 客户端初始序列号seq = 1
  • 服务器初始确认号ack = 1

第一步:建立连接(三次握手)

客户端发送 SYN 报文段:

  • 客户端服务器SYNseq = 1
  • 客户端状态SYN-SENT

服务器发送 SYN-ACK 报文段:

  • 服务器客户端SYN, ACKseq = 1ack = 2
  • 服务器状态SYN-RECEIVED

客户端发送 ACK 报文段:

  • 客户端服务器ACKseq = 2ack = 2
  • 双方状态ESTABLISHED

连接建立后,开始数据传输。

第二步:数据传输

接下来,客户端将每次发送 100 个字节的数据,直到完成 500 个字节的数据传输。

第一次数据传输(100字节)

  • 客户端发送数据
    • 客户端服务器数据[1-100]seq = 2ack = 2
  • 服务器确认
    • 服务器客户端ACKseq = 2ack = 102(期望下一个数据的序号为 102)

第二次数据传输(100字节)

  • 客户端发送数据
    • 客户端服务器数据[101-200]seq = 102ack = 2
  • 服务器确认
    • 服务器客户端ACKseq = 2ack = 202

第三次数据传输(100字节)

  • 客户端发送数据
    • 客户端服务器数据[201-300]seq = 202ack = 2
  • 服务器确认
    • 服务器客户端ACKseq = 2ack = 302

四次数据传输(100字节)

  • 客户端发送数据
    • 客户端服务器数据[301-400]seq = 302ack = 2
  • 服务器确认
    • 服务器客户端ACKseq = 2ack = 402

第五次数据传输(100字节)

  • 客户端发送数据
    • 客户端服务器数据[401-500]seq = 402ack = 2
  • 服务器确认
    • 服务器客户端ACKseq = 2ack = 502

数据传输完成,客户端共发送了 500 个字节的数据,服务器接收并确认所有数据。

第三步:关闭连接(四次挥手)

客户端发送 FIN 报文段:

  • 客户端服务器FINseq = 502ack = 2
  • 客户端状态FIN-WAIT-1

服务器发送 ACK 报文段:

  • 服务器客户端ACKseq = 2ack = 503
  • 服务器状态CLOSE-WAIT
  • 客户端状态FIN-WAIT-2

服务器发送 FIN 报文段:

  • 服务器客户端FINseq = 2ack = 503
  • 服务器状态LAST-ACK

客户端发送 ACK 报文段:

  • 客户端服务器ACKseq = 503ack = 3
  • 客户端状态TIME-WAIT

等待 2MSL 后,客户端关闭连接:

  • 客户端状态CLOSED

  • 服务器状态CLOSED

  • 建立连接阶段:通过三次握手建立 TCP 连接。

  • 数据传输阶段:客户端每次发送 100 字节数据,总共发送 5 次,每次服务器返回一个 ACK 确认。

  • 关闭连接阶段:通过四次挥手安全关闭 TCP 连接,确保双方都没有未发送或未确认的数据。

3.3.2 可靠传输保障

累积确认

  • 定义:累积确认是一种 TCP 的确认机制,接收方发送的 ACK 报文段中的确认号(ack)表示的是接收方期望接收的下一个字节的序列号。
  • 作用:如果接收方已经成功接收到一个报文段,它会发送一个包含累积确认号的 ACK。这个确认号表示接收方已经成功接收了所有序列号小于该确认号的字节数据。
  • 例子
    • 假设发送方发送了序列号为 1-100 和 101-200 的两个数据段,如果接收方成功接收了这两个数据段,它会发送一个 ACK,其中 ack = 201,表示接收到序列号 1 到 200 的所有数据,期望接收到下一个数据段从 201 开始。

延时确认

  • 定义:延时确认是接收方在收到数据报文段后不会立即发送 ACK,而是等待一段时间(通常是几毫秒),以检查是否有其他报文段到达或是否有数据需要发送。
  • 作用:延时确认的机制是为了减少 ACK 报文段的数量,降低网络开销。
  • 例子
    • 接收方接收到多个数据段后,可以只发送一个 ACK 来确认这些数据段,而不是为每个数据段都发送一个 ACK,从而减少网络流量。

超时重传

  • 定义:发送方在发送数据后,会启动一个定时器。如果在预定的时间内没有收到接收方的 ACK,发送方会假设该数据段丢失,并重新发送该数据段。
  • 作用:确保数据段能够可靠地到达接收方,即使在网络不稳定的情况下也能保证数据传输的完整性。
  • 例子
    • 发送方发送一个序列号为 1-100 的数据段,如果在一定时间内没有收到 ACK,它会重新发送这个数据段,直到收到确认或者达到最大重传次数。

这些机制共同作用,确保 TCP 连接在不可靠的网络条件下能够进行可靠的数据传输:

  • 累积确认 确保接收方能一次确认多个数据段,简化了确认过程。
  • 延时确认 降低了网络开销,提高了网络效率。
  • 超时重传 保证了数据传输的可靠性,即使发生数据丢失或延迟。

3.3.3 接收窗口

滑动窗口

3.3.4 拥塞控制

慢启动

拥塞避免

3.3.5 发送窗口

3.3 TCP 开发

3.3.1 套接字编程

socket() 创建套接字

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

// 创建套接字
int socket(int domain, int type, int protocol);

功能:
    创建一个网络套接字,用于在不同主机间进行数据传输。

参数:
    domain: 协议族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
    type: 套接字类型,如 SOCK_STREAM(面向连接的TCP)或 SOCK_DGRAM(无连接的UDP)。
    protocol: 协议编号,通常为 0,表示选择默认协议。

返回值:
    成功: 返回套接字描述符(正整数)。
    失败: 返回 -1,并设置 errno 以指示错误类型。

bind() 绑定地址

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

// 绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


功能:
    将套接字绑定到指定的地址和端口,常用于服务器程序,确定套接字接收数据的接口。

参数:
    sockfd: 套接字描述符,由 socket() 函数创建。
    addr: 指向包含地址信息的结构体(如 `struct sockaddr_in`)的指针。

    addrlen: 地址结构体的大小(字节数)。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 errno 以指示错误类型。

struct sockaddr 通用网络地址结构

结构体原型

struct sockaddr {
    unsigned short sa_family;  /* 地址族 */
    char sa_data[14];          /* 14 字节的协议地址 */
};

成员介绍

sa_family:
表示地址族,用于指定地址的类型。例如:
    - `AF_INET`:表示 IPv4 地址。
    - `AF_INET6`:表示 IPv6 地址。
    - `AF_LOCAL`:用于本地通信(UNIX 域协议)。

sa_data:
    是一个 14 字节的字符数组,用于存储协议地址。具体的内容取决于地址族(`sa_family`)的类型。例如,当使用 IPv4 地址时,`sa_data` 中包含端口号和 IP 地址。

struct sockaddr_in IPv4 地址结构

结构体原型

struct sockaddr_in {
    short int sin_family;          /* 地址族 */
    unsigned short int sin_port;   /* 端口号 */
    struct in_addr sin_addr;       /* IP 地址 */
};

struct in_addr {
    uint32_t       s_addr;     /* IP地址  网络字节序二进制 */
};

成员介绍

sin_family:
    地址族,与 struct sockaddr 中的 sa_family 相同。通常设置为 AF_INET,表示使用 IPv4 协议。

sin_port:
    表示端口号,使用 htons() 函数将主机字节序转换为网络字节序(大端序)。

sin_addr:
    是一个 struct in_addr 类型的结构体,用于存储 IPv4 地址。in_addr 结构体通常只有一个字段:
    struct in_addr {
        unsigned long s_addr;  /* 32 位 IPv4 地址 */
    };
    s_addr 通常使用 inet_aton() 或 inet_addr() 函数将点分十进制字符串转换为网络字节序的整数形式。

sin_zero:
    填充字段,用于确保 struct sockaddr_in 的大小与 struct sockaddr 相同。这个字段通常用 0 填充,没有特殊意义,只是为了对齐结构体的大小。

listen() 监听连接

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

// 监听连接
int listen(int sockfd, int backlog);


功能:
    将套接字设置为被动模式,监听来自客户端的连接请求,通常用于服务器程序。

参数:
    sockfd: 套接字描述符,由 socket() 函数创建并绑定。
    backlog: 挂起连接队列的最大长度,表示能同时处理的最大连接数。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 errno 以指示错误类型。

accept() 接受连接

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

// 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:
    接受一个来自监听套接字的连接请求,创建一个新的连接套接字用于通信。

参数:
    sockfd: 监听套接字描述符,由 listen() 函数启用监听的套接字。    
    addr: 指向存储客户端地址信息的结构体的指针 如果不关心 可以传NULL。
    addrlen: 指向存储客户端地址结构体大小的变量的指针 如果不关心 可以传NULL。

返回值:
    成功: 返回新的连接套接字描述符(正整数)。   
    失败: 返回 -1,并设置 errno 以指示错误类型。

connect() 发起连接

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

// 发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:
    主动发起到服务器的连接请求,通常用于客户端程序。

参数:
    sockfd: 套接字描述符,由 socket() 函数创建。
    addr: 指向包含服务器地址信息的结构体(如 struct sockaddr_in)的指针。
    addrlen: 服务器地址结构体的大小(字节数)。

返回值:
    成功: 返回 0。
    失败: 返回 -1,并设置 errno 以指示错误类型。

send() 发送数据

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

// 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);


功能:
    向已连接的套接字发送数据,通常用于客户端或服务器之间的数据传输。

参数:
    sockfd: 已连接的套接字描述符。
    buf: 指向要发送的数据缓冲区的指针。
    len: 要发送的数据字节数。
    flags: 发送控制标志,通常为 0。

返回值:
    成功: 返回实际发送的字节数。
    失败: 返回 -1,并设置 errno 以指示错误类型。

recv() 接收数据

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

// 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);


功能:
    从已连接的套接字接收数据,通常用于客户端或服务器之间的数据传输。

参数:
    sockfd: 已连接的套接字描述符。
    buf: 指向接收数据缓冲区的指针。
    len: 要接收的数据的最大字节数。
    flags: 接收控制标志,通常为 0。

返回值:
    成功: 返回实际接收到的字节数。
    失败: 返回 -1,并设置 errno 以指示错误类型。
    连接关闭: 返回 0,表示远程主机已关闭连接。

3.3.2 TCP开发流程

TCP编程流程

服务器端流程

​ 1、创建套接字

​ 2、填充服务器网络信息结构体

​ 3、将套接字与服务器的网络信息结构体绑定

​ 4、将套接字设置城 监听状态

​ 5、阻塞等待客户端连接 accept

​ 6、数据手法 write read

​ 7、关闭套接字 close

客户端流程

​ 1、创建流式套接字

​ 2、填充服务器网络信息结构体

​ 3、与服务器建立连接 cnnect

​ 4、关闭套接字 close

3.3.3 TCP开发示例

server.c

#include <head.h>

int main(int argc, char const *argv[])
{
    // 1、创建套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    OPEN_ERR(sock_fd);

    // 2、填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr , 0 , sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 端口号可以随意指定
    // IP 地址不能乱填 填写自己主机的IP地址
    // 如果是本机测试 则使用 127.0.0.1 本地回环地址  192.168.110.131
    server_addr.sin_addr.s_addr = inet_addr("192.168.110.131");

    socklen_t sock_len = sizeof(server_addr);

    // 3、将套接字与服务器的网络信息结构体绑定
    if (-1 == bind(sock_fd , ( struct sockaddr * )&server_addr , sock_len ))
    {
        perror("bind error:");
        return -1;
    }

    // 4、将套接字设置城 监听状态
    listen(sock_fd , 5);

    // 5、阻塞等待客户端连接 accept
    printf("等待客户端接入 ...\n");
    int sock_acc_fd = sock_acc_fd = accept(sock_fd , NULL , NULL );
    printf("等待客户端接入 成功\n");

    // 6、数据收发 write read
        // 接收数据
    char buf[128] = {0};
    read(sock_acc_fd , buf , 128);
    printf("客户端发来数据%s\n" , buf);
    int value_1 = 0;
    int value_2 = 0;
    int ret;
    char op = 0;

    sscanf( buf , "%d%c%d" , &value_1 , &op , &value_2);
    switch (op)
    {
    case '+':
        ret = value_1 + value_2;
        break;

    case '-':
        ret = value_1 - value_2;
        break;
    default:
        break;
    }

    memset(buf , 0 , 128);

    sprintf( buf , "%d %c %d = %d\n" , value_1 , op , value_2 , ret);

    write( sock_acc_fd , buf , sizeof(buf));

    // 7、关闭套接字  close
    close(sock_acc_fd);
    close(sock_fd);

    return 0;
}

client.c

#include <head.h>

int main(int argc, char const *argv[])
{
    // 1、创建套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    OPEN_ERR(sock_fd);

    // 2、填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr , 0 , sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);  // 端口号可以随意指定
    // IP 地址不能乱填 填写自己主机的IP地址
    // 如果是本机测试 则使用 127.0.0.1 本地回环地址  192.168.110.131   
    server_addr.sin_addr.s_addr = inet_addr("192.168.110.131");

    socklen_t sock_len = sizeof(server_addr);

    // 3、与服务器建立连接 cnnect
    // 等待连接
    printf("等待连接\n");

        connect(sock_fd , (struct sockaddr * ) & server_addr , sock_len );


    // 4、数据收发 write read

        // 接收数据

    char buf[128] = {0};

    fgets( buf , 128 , stdin );

    write(sock_fd , buf , sizeof(buf));

    memset(buf , 0 , 128);

    read(sock_fd , buf , 128);

    // 5、关闭套接字  close
    close(sock_fd);

    return 0;
}

3.4 桥接模式 和 NAT模式的区别

在虚拟机中 的虚拟网卡 有两种 常用模式

NAT和桥接模式的区别

端口转发流程