Linux基础 (十四):socket网络编程

         我们用户是处在应用层的,根据不同的场景和业务需求,传输层就要为我们应用层提供不同的传输协议,常见的就是TCP协议和UDP协议,二者各自有不同的特点,网络中的数据的传输其实就是两个进程间的通信,两个进程在通信时,传输层使用TCP协议将一方进程的应用层的数据传输给另一方进程的应用层,我们这一节就是基于TCP协议讲解网络数据的传输。

目录

一、主机字节序列和网络字节序列

1.1 概念

1.2 接口函数

二、套接字地址结构

2.0 套接字

2.1 通用 socket 地址结构

2.2 专用 socket 地址结构

2.3 IP 地址转换函数

三、网络编程接口

3.1 创建套接字(买个手机)

3.2  套接字地址绑定(为手机办卡,电话号码相当于地址)

3.3 从监听队列中接收一个连接(开机)

3.4 接受客户端连接请求并创建新的套接字 (接听电话)

3.5 客户端主动与服务器建立连接

3.6 从已连接的套接字中接收数据(TCP数据读)

3.6 发送数据到已连接的套接字(TCP数据写)

3.7 从已连接或未连接的套接字接收数据(UDP数据读)

3.8 通过套接字发送数据到指定目标地址((UDP数据写)

3.9 关闭套接字

四、TCP 编程流程

五、三次握手和四次挥手(重点面试题)

5.1 三次握手

5.2 可以将三次握手改成两次握手吗?

5.3 四次挥手

5.4 可以将四次挥手改成三次挥手吗?

六、tcp协议服务器-客户端编程流程实验(掌握)

6.1 服务器端

6.2 客户端

七、实验改进

7.1 服务器端

7.2 客户端


一、主机字节序列和网络字节序列

1.1 概念

        主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。这是为了确保不同系统之间的数据传输一致性。无论主机字节序列是什么,数据在网络上传输时都需要转换为网络字节序。

  1. 大端字节序是指:一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址 处。
  2. 小端字节序是指:整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的 低地址处。

1.2 接口函数

          为了在主机字节序列和网络字节序列之间进行转换,编程语言通常提供了一些标准函数。Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:

#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序

二、套接字地址结构

2.0 套接字

        套接字(Socket)是网络编程中用于描述IP地址和端口的通信端点。它是网络通信中的一个抽象概念,通常用于描述两个程序之间的双向通信链路。套接字是网络编程的基石,允许应用程序通过网络发送和接收数据。

套接字的类型

套接字主要有两种类型:

  1. 流式套接字(Stream Socket)

    • 使用TCP(传输控制协议)
    • 提供面向连接的、可靠的、基于字节流的通信。
    • 典型应用场景包括HTTP、FTP、SMTP等协议。
  2. 数据报套接字(Datagram Socket)

    • 使用UDP(用户数据报协议)
    • 提供无连接的、不可靠的、基于数据报的通信。
    • 适用于需要快速传输且允许丢包的场景,如视频流、在线游戏等。

2.1 通用 socket 地址结构

       在网络编程中,通用的 socket 地址结构(Socket Address Structure)用于存储网络地址信息。不同的协议族(如 IPv4、IPv6 等)有不同的地址结构,但都遵循一个通用的框架。socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,这是所有地址结构的通用基础,定义在 <sys/socket.h> 头文件中。它是一个通用的地址结构,包含了地址族信息。

struct sockaddr {
    sa_family_t sa_family;  // 地址族(Address Family)
    char sa_data[14];       // 协议地址(Protocol Address)
};

地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:

2.2 专用 socket 地址结构

        TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:

sockaddr_in 结构体(用于 IPv4)

   sockaddr_in 结构体用于存储 IPv4 地址信息它定义在 <netinet/in.h> 头文件中。结构体定义如下:

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族(必须为 AF_INET)
    in_port_t      sin_port;     // 端口号(使用 `htons` 将主机字节序转换为网络字节序)
    struct in_addr sin_addr;     // IP 地址(使用 `inet_pton` 等函数进行赋值)
    char           sin_zero[8];  // 填充字段,使得结构体大小与 `sockaddr` 一致
};
  • sin_family:地址族,必须设置为 AF_INET
  • sin_port:端口号,必须使用 htons 函数将主机字节序转换为网络字节序。
  • sin_addr:IP 地址,通常使用 inet_ptoninet_aton 函数进行设置。
  • sin_zero:填充字段,使得结构体大小与 sockaddr 结构体一致,通常设置为 0。

sockaddr_in6 结构体(用于 IPv6)

   sockaddr_in6 结构体用于存储 IPv6 地址信息。它也定义在 <netinet/in.h> 头文件中。结构体定义如下:

struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族(必须为 AF_INET6)
    in_port_t       sin6_port;     // 端口号(使用 `htons` 将主机字节序转换为网络字节序)
    uint32_t        sin6_flowinfo; // 流信息(通常设置为 0)
    struct in6_addr sin6_addr;     // IPv6 地址
    uint32_t        sin6_scope_id; // 范围 ID(用于本地链路地址,通常设置为 0)
};
  • sin6_family:地址族,必须设置为 AF_INET6
  • sin6_port:端口号,必须使用 htons 函数将主机字节序转换为网络字节序。
  • sin6_flowinfo:流信息,通常设置为 0。
  • sin6_addr:IPv6 地址,使用 inet_pton 函数进行设置。
  • sin6_scope_id:范围 ID,主要用于本地链路地址,通常设置为 0。

2.3 IP 地址转换函数

      通常,人们习惯用点分(用点分隔)十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数(4个字节32位)方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换(字符串转整型函数接口):

需要引入的头文件 #include <arpa/inet.h>

in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为无符号整型
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序(无符号整型)转化为字符串表示

三、网络编程接口

3.1 创建套接字(买个手机)

int socket(int domain, int type, int protocol);

 参数解释

  1. domain: 指定套接字所使用的协议族,也称为地址族。常见的值包括:

    • AF_INET:IPv4 Internet 协议
    • AF_INET6:IPv6 Internet 协议
    • AF_UNIXAF_LOCAL:本地通信(UNIX 域套接字)
    • AF_PACKET:低级别的套接字接口,用于直接访问网络层
  2. type: 指定套接字的服务类型。常见的类型包括:

    • SOCK_STREAM:提供面向连接的可靠字节流服务(如 TCP)
    • SOCK_DGRAM:提供数据报服务(如 UDP)
    • SOCK_RAW:提供原始网络协议访问
    • SOCK_SEQPACKET:提供序列包服务,类似于 SOCK_STREAM,但每个消息边界保留
  3. protocol: 指定使用的协议。通常设置为 0,以选择默认协议。可以明确指定特定协议:

    • IPPROTO_TCP:如果 type 是 SOCK_STREAM
    • IPPROTO_UDP:如果 type 是 SOCK_DGRAM
    • IPPROTO_ICMP:如果 type 是 SOCK_RAW(用于原始套接字)

返回值

       成功时返回一个非负整数,即套接字文件描述符。 失败时返回 -1,并设置 errno 以指示错误。

3.2  套接字地址绑定(为手机办卡,电话号码相当于地址)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

        它是一个用于将套接字绑定到特定的本地地址和端口的系统调用。在网络编程中,bind 函数通常用于服务器端套接字,以指定它们将在哪个地址和端口上监听传入连接

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. addr:

    • 这是一个指向 struct sockaddr 的指针(通用套接字地址结构体指针),包含要绑定到的地址信息。实际的地址结构根据使用的协议族不同而不同
      • 对于 IPv4,使用 struct sockaddr_in
      • 对于 IPv6,使用 struct sockaddr_in6
      • 对于本地通信(UNIX 域套接字),使用 struct sockaddr_un
  3. addrlen:

    • 这是地址结构的长度(以字节为单位)。利用sizeof求得即可,对于 IPv4 地址,通常是 sizeof(struct sockaddr_in);对于 IPv6 地址,通常是 sizeof(struct sockaddr_in6)

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误。

3.3 从监听队列中接收一个连接(开机)

int listen(int sockfd, int backlog);

      它是一个用于在指定的套接字上监听连接请求的系统调用。它通常用于服务器端的套接字,以便将套接字转换为被动模式,准备接受来自客户端的连接请求。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建并绑定了地址(通过 bind 函数)的套接字文件描述符
  2. backlog:

    • accept 函数被调用之前可以排队的连接请求数量。在Linux系统上指的是已经完成三次握手的客户端的数量,在unix系统上指的是未完成加已完成的客户端数量。
    • 如果连接请求的数量超过了此限制,新来的连接请求将被拒绝。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误。

       监听队列可以理解为:客户端向服务器端发送连接请求时,首先,先将它放到监听队列中,让它等着,然后服务器一个一个的从监听队列进行连接,相当于银行大厅的等待区,监听队列的大小就是为等待客户提供的凳子的数量。 

3.4 接受客户端连接请求并创建新的套接字 (接听电话)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       它是一个用于接受传入连接请求的系统调用。它通常用于服务器端套接字,用于接受客户端连接请求并创建新的套接字用于与客户端通信。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建并绑定了地址(通过 bind 函数)的套接字文件描述符。
  2. addr:

    • 这是一个指向 struct sockaddr 类型的指针用于存储连接的远程地址信息(客户端的套接字地址信息)。可以将其设置为 NULL,如果不关心连接的远程地址信息。
  3. addrlen:

    • 这是一个指向 socklen_t 类型的指针,指示传入的地址结构的长度。在调用 accept 函数之前,应该将其设置为 struct sockaddr 结构的大小。

返回值

  • 如果成功,返回一个新的套接字文件描述符,也就是连接套接字,用于与客户端通信。
  • 如果失败,返回 -1,并设置 errno 以指示错误。

3.5 客户端主动与服务器建立连接

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

      它是一个用于连接到远程服务器的系统调用。它通常用于客户端套接字,用于与服务器建立连接。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. serv_addr:

    • 这是一个指向 struct sockaddr 类型的指针,包含远程服务器的地址信息
  3. addrlen:

    • 这是传入地址结构的长度(以字节为单位)

返回值

  • 如果成功建立连接,则返回 0。
  • 如果失败,返回 -1,并设置 errno 以指示错误。

3.6 从已连接的套接字中接收数据(TCP数据读)

ssize_t recv(int sockfd, void *buff, size_t len, int flags);

     它是一个用于从套接字接收数据的系统调用。它通常用于在网络编程中从已连接的套接字中接收数据。recv的返回值如果等于0,说明对方关闭了!!!这是循环收发判断的唯一条件!

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. buff:

    • 这是一个指向接收数据缓冲区的指针,用于存储接收到的数据。
  3. len:

    • 这是接收数据缓冲区的长度,即 buff 所指向的缓冲区的大小
  4. flags:

    • 这是一组控制接收行为的标志,可以为 0 或以下之一的按位或:
      • MSG_WAITALL:阻塞直到接收到指定长度的数据。
      • MSG_DONTWAIT:非阻塞接收数据。

返回值

  • 如果成功接收到数据,则返回接收到的字节数。
  • 如果连接已关闭,则返回 0。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误。

3.6 发送数据到已连接的套接字(TCP数据写)

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

       它是一个用于将数据通过套接字发送到远程端的系统调用。通常在网络编程中,它被用于发送数据到已连接的套接字上。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. buff:

    • 这是一个指向要发送数据的缓冲区的指针
  3. len:

    • 这是要发送的数据的字节数
  4. flags:

    • 这是一组控制发送行为的标志,可以为 0 或以下之一的按位或:
      • MSG_CONFIRM:要求数据发送得到确认。
      • MSG_DONTROUTE:数据不路由,仅限于本地接收。
      • MSG_EOR:数据结束标志。
      • MSG_MORE:还有更多数据等待发送。
      • MSG_NOSIGNAL:忽略 SIGPIPE 信号,如果连接已关闭,则不会引发信号。

返回值

  • 如果成功发送数据,则返回实际发送的字节数。
  • 如果发送过程中出现错误,则返回 -1,并设置 errno 来指示错误。

3.7 从已连接或未连接的套接字接收数据(UDP数据读)

ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen);

      它是一个用于从已连接或未连接的套接字接收数据的系统调用。与 recv 不同的是,recvfrom 可以从任意地址接收数据,而不仅仅是连接到套接字的对等方。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. buff:

    • 这是一个指向接收数据缓冲区的指针,用于存储接收到的数据。
  3. len:

    • 这是接收数据缓冲区的长度,即 buff 所指向的缓冲区的大小。
  4. flags:

    • 这是一组控制接收行为的标志,可以为 0 或以下之一的按位或:
      • MSG_WAITALL:阻塞直到接收到指定长度的数据。
      • MSG_DONTWAIT:非阻塞接收数据。
      • MSG_TRUNC:截断超出缓冲区大小的数据。
  5. src_addr:

    • 这是一个指向存储发送端地址信息的 struct sockaddr 结构体的指针。
  6. addrlen:

    • 这是传入地址结构的长度(以字节为单位)。在调用 recvfrom 函数之前,应该将其设置为 struct sockaddr 结构的大小。

返回值

  • 如果成功接收到数据,则返回接收到的字节数。
  • 如果连接已关闭,则返回 0。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误。

3.8 通过套接字发送数据到指定目标地址((UDP数据写)

ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);

       它是一个用于通过套接字发送数据到指定目标地址的系统调用。与 send 不同的是,sendto 允许指定目标地址,因此适用于无连接的 UDP 套接字以及有连接的套接字。

参数解释

  1. sockfd:

    • 这是由 socket 函数创建的套接字文件描述符。
  2. buff:

    • 这是一个指向要发送数据的缓冲区的指针。
  3. len:

    • 这是要发送的数据的字节数。
  4. flags:

    • 这是一组控制发送行为的标志,可以为 0 或以下之一的按位或:
      • MSG_CONFIRM:要求数据发送得到确认。
      • MSG_DONTROUTE:数据不路由,仅限于本地发送。
      • MSG_EOR:数据结束标志。
      • MSG_MORE:还有更多数据等待发送。
      • MSG_NOSIGNAL:忽略 SIGPIPE 信号,如果连接已关闭,则不会引发信号。
  5. dest_addr:

    • 这是一个指向包含目标地址信息的 struct sockaddr 结构体的指针。
  6. addrlen:

    • 这是传入目标地址结构的长度(以字节为单位)。在调用 sendto 函数之前,应该将其设置为 struct sockaddr 结构的大小。

返回值

  • 如果成功发送数据,则返回实际发送的字节数。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误。

3.9 关闭套接字

int close(int sockfd);

它是一个用于关闭套接字的系统调用。关闭套接字后,不再可以使用该套接字进行数据传输或接收。

参数解释

  • sockfd:
    • 这是要关闭的套接字的文件描述符。

返回值

  • 如果成功关闭套接字,则返回 0。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误。

四、TCP 编程流程

TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如 下:

1、socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。

         这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。

2、bind()方法是用来指定套接字使用的 IP 地址和端口。

         IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整型值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。

3、listen()方法是用来创建监听队列。

         监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。

         在网络编程中,服务器端通过监听指定的网络地址和端口来等待客户端的连接请求。

监听队列就像是一个等待区,它存放着已经发送连接请求但还没有得到服务器响应的客户端连接请求。当一个客户端请求连接时,服务器将其放入监听队列中,然后按照一定的顺序逐个处理这些请求。通俗地说,你可以把监听队列想象成是一个餐厅的等候区。当你到达餐厅时,可能会看到一个等候区,里面坐满了等待就座的人。服务员会按照先来后到的顺序逐个安排客人入座,就像服务器按照监听队列中连接请求的顺序逐个处理客户端的连接请求一样。

4、accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。

       每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。

5、connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。

      该方法执行后,会进行三次握手, 建立连接。

6、send()方法用来向 TCP 连接的对端发送数据。

      send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。

7、recv()方法用来接收 TCP 连接的对端发送来的数据。

        recv()从本端的接收缓冲区中读取数 据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。

8、close()方法用来关闭 TCP 连接。

     此时,会进行四次挥手。

五、三次握手和四次挥手(重点面试题)

5.1 三次握手

          客户端在进行connect()开始建立连接 之后就会进行三次握手!

        三次握手是TCP/IP协议中用于建立可靠连接的过程。在进行通信之前,客户端和服务器之间需要通过三次握手来确认彼此的通信能力和参数设置。这个过程包括以下步骤:

  1. 客户端发送同步(SYN)报文:客户端首先向服务器发送一个带有SYN标志的TCP报文段,表示客户端想要建立连接,并且指定初始序列号(sequence number)。

  2. 服务器确认同步(SYN-ACK)报文:服务器收到客户端的SYN报文后,会向客户端发送一个带有SYN和ACK标志的TCP报文段作为确认。该报文段中也包含服务器选择的初始序列号。

  3. 客户端确认(ACK)报文:最后,客户端收到服务器的SYN-ACK报文后,会向服务器发送一个带有ACK标志的TCP报文段作为确认。这个报文段不携带SYN标志。

完成了这三次握手之后,客户端和服务器之间的连接就建立起来了,双方可以开始进行数据传输。这个过程确保了双方都能够收到彼此的确认,从而建立了可靠的通信连接。

下面为面试内容!!     

        在完成握手时,有两个队列,一个是未完三次握手队列,一个是已完成三次握手队列,客户端请求连接,首先会放到未完三次握手队列,然后等他完成三次握手队列,也就是建立好连接以后,就会将它放到已完成三次握手队列,(注意:listen(socked,5) 在linux里这里的5是代表已完成三次握手队列的大小,在unix里代表未完成和已完成队列之和。这里的5,不是说只能完成5次握手,而是完成握手队列里能放5个链接,第六个就放在未完成队列里,等到完成握手队列里有空位了,在挪下来 ),然后在进行accept()的时候,它会去已完成三次握手队列的里面去看,如果有已经完成三次握手队列的客户端请求,那么他就会与该客户端建立连接,产生一个连接套接字,否则,他会一直阻塞住!accept只处理已完成握手队列中的链接

三次握手发生在客户端执行 connect()的时候,该方法返回成功,则说明三次握手已经建 立。三次握手示例图如下: 

 

现在解释这个图:

  1. 客户端首先向服务器发送一个带有 SYN(同步)标志的报文,表示客户端想要建立连接,并指定初始序列号。这是第一次握手。

  2. 服务器收到客户端的 SYN 报文后,会发送一个带有 SYN 和 ACK(确认)标志的报文给客户端,表示服务器收到了客户端的请求,并同意建立连接,同时服务器也指定了自己的初始序列号。这是第二次握手。

  3. 客户端收到服务器的 SYN-ACK 报文后,发送一个带有 ACK 标志的报文给服务器,表示客户端确认收到了服务器的确认,并同意建立连接。这是第三次握手。

5.2 可以将三次握手改成两次握手吗?

      不可以!根据TCP协议的设计,三次握手是必需的,并且是建立可靠连接的基础。在标准的TCP实现中,无法将三次握手简化为两次握手。这是因为第三次握手中客户端必须发送ACK包来确认连接建立,以确保双方都能够收到对方的确认信息。

5.3 四次挥手

       执行close()之后就会进行四次挥手操作,服务器端和客服端那一端先close()都可以!

三次挥手是TCP/IP协议中用于关闭连接的过程。与建立连接时的三次握手相似,关闭连接时需要进行四次挥手以确保双方都能够完成数据传输并关闭连接,这个过程包括以下步骤:

  1. 客户端发送关闭请求(FIN):当客户端决定关闭连接时,它会发送一个带有FIN标志的TCP报文段给服务器,表示它不再发送数据了,但仍然可以接收数据。

  2. 服务器确认关闭请求(ACK):服务器收到客户端的关闭请求后,会发送一个带有ACK标志的TCP报文段作为确认,表示它已经收到了客户端的关闭请求。

  3. 服务器发送关闭请求(FIN):当服务器确定不再发送数据时,它也会向客户端发送一个带有FIN标志的TCP报文段,表示它也准备关闭连接。

  4. 客户端确认关闭请求(ACK):客户端收到服务器的关闭请求后,会发送一个带有ACK标志的TCP报文段作为确认。此时,双方的连接就被完全关闭了。

通过这个四次挥手的过程,客户端和服务器都有机会告知对方它们不再发送数据,并且确认对方的关闭请求,从而安全地关闭连接,避免数据丢失或不完整的传输。

四次挥手发生在客户端或服务端执行 close()关闭连接的时候,示例图如下: 

 

这里是四次挥手的解释:

  1. 客户端首先发送一个带有 FIN(关闭请求)标志的报文给服务器,表示客户端不再发送数据,但仍然能接收数据。这是第一次挥手。

  2. 服务器收到客户端的 FIN 报文后,发送一个带有 ACK(确认)标志的报文给客户端,表示服务器已经收到了客户端的关闭请求,但服务器还可以向客户端发送数据。这是第二次挥手。

  3. 服务器在确定不再发送数据后,发送一个带有 FIN(关闭请求)标志的报文给客户端,表示服务器也准备关闭连接。这是第三次挥手。

  4. 客户端收到服务器的 FIN 报文后,发送一个带有 ACK(确认)标志的报文给服务器,表示客户端确认收到了服务器的关闭请求。这是第四次挥手。

5.4 可以将四次挥手改成三次挥手吗?

       可以,四次挥手可以演化成三次挥手 当一端close 发送报文过来,此时我也要close了,回复报文,和通知对方关闭的报文一起发送。

  • 第一次挥手(FIN): 客户端发送一个FIN报文,表示它要关闭到服务器的数据传送。
  • 第二次挥手(FIN): 服务器收到FIN后,直接发送一个FIN报文,表示它也要关闭到客户端的数据传送。
  • 第三次挥手(ACK): 客户端收到FIN后,发送一个ACK报文,确认收到关闭请求,连接关闭。

六、tcp协议服务器-客户端编程流程实验(掌握)

6.1 服务器端

      简单的TCP服务器,它监听6000端口,接收来自客户端的消息,回复“ok”并关闭连接。服务器无限运行,一次处理一个客户端。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  //创建套接字   ----->监听套接字(相当于饭店的接待员)
    if( sockfd == -1 )
    {
        exit(1);
    }

    struct sockaddr_in saddr,caddr;                                   //定义服务器套接字地址,客户端套接字地址
    memset(&saddr,0,sizeof(saddr));                                 //清零套接字地址结构体的第四个成员

    /*为服务器套接字地址结构体初始化*/
    saddr.sin_family = AF_INET;                                      //地址族   IPV4     
    saddr.sin_port = htons(6000);                                    //端口号   6000
    saddr.sin_addr.s_addr = inet_addr("43.138.164.79");                  //服务器IP地址;回环地址(用的是测试的本机)

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));    //套接字地址绑定
    if ( res == -1 )
    {
        printf("bind err\n");                                        //这里容易失败,所以要打印观察
        exit(1);
    }

    if (listen(sockfd,5) == -1 )                                     //从监听队列中接收一个连接
    {
        exit(1);             
    }


    //服务器无限运行,一次处理一个客户端。
    while( 1 )                                                      //循环接收连接
    {
        int len = sizeof(caddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);   //返回值为连接套接字(得到新的套接字描述符),没有人连接时,可能会阻塞!(没有客人来)
        if (c < 0 )                                             //c是套接字文件描述符,相当于服务员,连接失败
        {
            continue;
        }

        printf("accept c =%d\n",c );

        char buff[128] = {0};                                    
        int n = recv(c,buff,127,0);                           //接收客户端发送过来的数据,如果客户端未发送数据,此时便会阻塞! 读取最多127字节,以留出一个字节用于\0终止符
        printf("recv=%s\n",buff);                             
        send(c,"ok",2,0);                                     //服务器给客户端发送数据
        close(c);                                             //关闭本次与客户端连接的套接字描述符
    }
}

6.2 客户端

       实现了一个简单的TCP客户端,它连接到IP地址为 43.138.164.79、端口为 6000 的服务器,读取用户输入,将输入发送到服务器,接收服务器的响应并打印,然后关闭连接。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);                   //创建套接字---->监听套接字
    if (sockfd == -1 )
    {
        exit(1);
    }

    /**注意:客户端不需要绑定套接字地址(调用bind()函数)端口号会随机分配,IP地址就直接用*/

    
    /*客户端需要连接服务器端,因此需要指定连接的服务器的套接字地址,下面的都是服务器的套接字地址信息*/
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;                                    //地址族
    saddr.sin_port = htons(6000);                                  //服务器的端口号
    saddr.sin_addr.s_addr = inet_addr("43.138.164.79");            //服务器IP地址


    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));  //连接服务器,失败概率高,打印显示
    if (res == -1 )
    {
        printf("connect err\n");
        exit(1);
    }


    char buff[128] = {0};
    printf("input:\n");
    fgets(buff,128,stdin);

    send(sockfd,buff,strlen(buff)-1,0);                         //客户端发送数据给服务器,客户端不分监听套接字和连接套接字
    memset(buff,0,128);
    recv(sockfd,buff,127,0);                                    //客户端接收服务器发送过来的数据
    printf("buff=%s\n",buff);
    close(sockfd);
    exit(0);
}

运行有先后顺序,先运行服务器端,在运行客户端。

七、实验改进

7.1 服务器端

一旦有客户端连接成功,便会一直建立连接,循环收发数据!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int sockfd = socket_init();
    if (sockfd == -1)
    {
        printf("create socket failed\n");
        exit(1);
    }

    while( 1 )
    {
        struct sockaddr_in  caddr;
        int len = sizeof(caddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//阻塞
        if (c < 0 )
        {
            continue;
        }


        //上面一旦有客户端连接成功,便会进行下面的循环数据收发
        /****改动之处:一直建立连接,可以循环收发数据****/
        while ( 1 )
        {
            char buff[128] = {0};
            int n = recv(c,buff,127,0);  //可能会阻塞
            if(n<=0)                    //n等于0说明对方关闭,n小于0说明出错了
            { 
                 break;               //对方关闭后,不需要进行通信了
            }
            printf("recv(c=%d)=%s\n",c,buff);
            send(c,"ok",2,0);
                      
        }
        close(c);                   //服务器端也应该关闭与该客户端进行通信
    }
}



//封装创建套接字并绑定,进行监听的函数
int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//tcp 
    if (sockfd == -1)
    {
        exit(1);
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;               //地址族 ipv4
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");

    int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (res == -1)
    {
        printf("bind err\n");
        exit(1);
    }

    if (listen(sockfd, 5) == -1)
    {
        exit(1);
    }

    return sockfd;

}

7.2 客户端

客户端连接成功,便会一直建立连接,循环收发数据!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (sockfd == -1 )
    {
        exit(1);
    }

    //连接服务器
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);//ser 6000
    saddr.sin_addr.s_addr = inet_addr("43.138.164.79");

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if (res == -1 )
    {
        printf("connect err\n");
        exit(1);
    }

    printf("连接成功!\n");


    //****客户端可以循环进行发送***/
    while(1)
    { 
        char buff[128] = {0};
        printf("input:\n");
        fgets(buff,128,stdin);

        if (strncmp(buff, "end", 3) == 0)
        {
            break;
        }
        send(sockfd,buff,strlen(buff)-1,0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("buff=%s\n",buff);
       
      
    }

    close(sockfd);
    exit(0);
}

至此,已经讲解完毕!篇幅较长,慢慢消化,以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/684045.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

activiti用法随记

案例&#xff1a; 摘抄于官网&#xff0c;假设我们有如下流程&#xff1a; 流程对应的bpmn文件如下&#xff1a; <definitions xmlns:activiti"http://activiti.org/bpmn" xmlns:bpmndi"http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc&quo…

configuration_auto.py in getitem raise KeyError(key) KeyError: ‘llama‘解决方案

运行LLaMA-7b模型有时候会报错“configuration_auto.py in getitem raise KeyError(key) KeyError:llama”如下所示&#xff1a; 解决办法升级安装transformer库即可&#xff0c;如下所示&#xff1a; pip install transformers4.30.0

vue3设置全局变量并获取 全局响应式变量 窗口大小

设置 js文件统一管理全局变量 方法1 app provide() 全局提供变量 通过inject()使用 方法2 app实例配置全局变量 获取 通过 getCurrentInstance.appContext.config.globalProperties.$innerWidth访问到 code import { ref } from vue export const useGlobalState () > {c…

新型AI编程语言Mojo来了!比Python快68000倍! 坚持每天写代码,真的能提高编程水平吗?

2024 年 3 月 29 日&#xff0c;Modular Inc. 宣布开源 Mojo 的核心组件。 Mojo语言是一种新的编程语言&#xff0c;由 Chris Lattner&#xff08;LLVM 和 Swift 语言的创始人&#xff09;创建的 Modular AI 公司开发。 它结合了Python的易用性和C的性能&#xff0c;旨在为人…

二说springboot3的自动配置机制

大家好&#xff0c;这里是教授.F 目录 SpringBootApplication&#xff1a; EableAutoConfiguration&#xff1a; 上一篇文章粗略的讲了自动配置机制&#xff0c;二说系列将从源码的角度进行讲解。 SpringBootApplication&#xff1a; 首先我们还是得从SpringBootApplication…

电商APP用户体验提升技巧:一个实战案例

随着网络和移动技术的快速发展&#xff0c;加上全球疫情的影响&#xff0c;电子商务应用程序改变了人们的购物方式&#xff0c;积累了大量的用户群体。如今&#xff0c;一个成功的电子商务应用程序&#xff0c;除了网站用户界面的美&#xff0c;电子商务用户体验的设计&#xf…

图片格式怎么转成pdf,简单的方法

在现代数字化时代&#xff0c;图片格式转换成PDF已经成为许多人的日常需求。无论是为了存档、分享还是打印&#xff0c;将图片转换为PDF都是一项非常实用的技能。本文将详细介绍如何将图片格式转换成PDF的方法。 用浏览器打开 "轻云处理pdf官网&#xff0c;上传图片。 图…

SpringBoot 请求响应

SpringBoot 请求响应 来源于黑马程序员JavaWeb课程&#xff0c;总结笔记 1.ApiFox Apifox快速入门教程 2.基本参数 简单参数&#xff1a;在向服务器发起请求时&#xff0c;向服务器传递的是一些普通的请求数据。 //RequestController.java import jakarta.servlet.http.Htt…

一线教师教学工具汇总

亲爱的教师们&#xff01;我们的教学工具箱里也该更新换代啦&#xff01;今天&#xff0c;就让我来给大家安利一波超实用的教学神器&#xff1a; 百度文库小程序 —— 在线图书馆 百度文库&#xff0c;一个宝藏级的在线文档分享平台&#xff01;在这里&#xff0c;你可以找到海…

康谋技术 | 自动驾驶:揭秘高精度时间同步技术(二)

在自动驾驶中&#xff0c;对车辆外界环境进行感知需要用到很多传感器的数据&#xff08;Lidar&#xff0c;Camera&#xff0c;GPS/IMU&#xff09;&#xff0c;如果计算中心接收到的各传感器消息时间不统一&#xff0c;则会造成例如障碍物识别不准等问题。 为了对各类传感器进…

VSCode插件EditorConfig for VS Code

1. 安装 2. 配置 # http://editorconfig.orgroot true[*] # 表示所有文件适用 charset utf-8 # 设置文件字符集为 utf-8 indent_style space # 缩进风格&#xff08;tab | space&#xff09; indent_size 4 # 缩进大小 end_of_line lf # 控制换行类型(lf | cr | crlf) tr…

鸿蒙低代码开发一个高频问题

在版本是DevEco Studio 3.1.1 Release&#xff0c;SDK是3.1.0(API9)。 创建和设计的visual文件经常会遇到无法渲染的情况&#xff0c;或者自定义组件在Custom列表中突然不见了的情况。 有以下报错信息的&#xff1a; JSON schema validation error: data/visualModel/value/…

Face Forgery Detection by 3D Decomposition

文章目录 Face Forgery Detection by 3D Decomposition研究背景研究目标创新点方法提出问题研究过程技术贡献实验结果未来工作Face Forgery Detection by 3D Decomposition 会议:CVPR2021 作者: 研究背景 面部伪造引发关注传统面部伪造检测主要关注原始RGB图像

React权限管理系统实现

目录 一、需求 二、逻辑 三、实现 &#xff08;一&#xff09;代码 &#xff08;二&#xff09;解释 1. 获取权限对照数组 (queryReferencePermissionsInfo) 2. 获取处理对照数组 (queryDisposePermissionsInfo) 3. 获取权限映射表信息并处理 (queryPermissionsInfo) 4…

【分享】两种方法设置PDF“打开密码”

想要保护PDF文件的私密性&#xff0c;只允许特定人查看&#xff0c;我们可以给PDF设置“打开密码”&#xff0c;这样只有知道密码的人才可以打开文件。如果小伙伴们不知道如何设置&#xff0c;就一起看看以下两种方法吧&#xff01; 方法1&#xff1a;使用PDF编辑器 大部分PD…

【异常检测】MVTec LOCO AD 数据集

every blog every motto: There’s only one corner of the universe you can be sure of improving, and that’s your own self. https://blog.csdn.net/weixin_39190382?spm1010.2135.3001.5343 0. 前言 MVTec LOCO AD 数据集介绍 相比MVTec AD 增加了逻辑异常 1. 正文…

【grafana】创建多变量table

这个普罗米修斯的指标啊&#xff0c;大多数都是键值对&#xff0c;而且笔者如果没记错&#xff0c;他这个值还必须是浮点。少数可以设成离散值&#xff08;Enum&#xff09;&#xff0c;但本质还是一个带翻译功能的键值对 这样的好处是&#xff0c;做起来非常简单&#xff0c;…

Java环境配置(超详细)

Java环境配置&#xff08;超详细&#xff09; 引言1、安装 JDK1.1、下载安装JDK1.2、配置环境变量&#xff1a;JAVA_HOME1.3、将JAVA_HOME添加到Path中 2、安装 Maven2.1、下载安装Maven2.2、配置maven的环境变量: M2_HOME2.3、将Maven变量添加到Path中 引言 Java开发环境的配…

智能水位监测识别报警摄像机:保障水域安全的重要利器

随着城市化进程的加速和气候变化的影响&#xff0c;对水域安全的关注日益增加。为了及时监测水位变化并采取相应措施&#xff0c;智能水位监测识别报警摄像机应运而生。本文将介绍这一创新技术的应用和优势。 传统的水位监测方法通常依赖于传感器&#xff0c;但其存在着安装位置…

如何编辑pdf文件内容?3种PDF编辑方法分享

如何编辑pdf文件内容&#xff1f;在当今数字化时代&#xff0c;PDF文件因其跨平台兼容性和保持原样不变的特点&#xff0c;在办公、学习、生活等多个领域得到了广泛应用。然而&#xff0c;PDF文件的不可编辑性也让许多用户感到困扰。你是否曾经遇到过需要修改PDF文件内容&#…