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);                                    //端口号   6000saddr.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;               //地址族 ipv4saddr.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 6000saddr.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);
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

32C3-2模组与乐鑫ESP32­-C3­-WROOM­-02模组原理图、升级口说明

模组原理图&#xff1a; 底板原理图&#xff1a; u1 是AT通信口&#xff0c;wiif-tx wifi-rx 是升级口&#xff0c;chip-pu是reset复位口&#xff0c;GPIO9拉低复位进入下载模式 ESP32-WROOM-32 系列硬件连接管脚分配 功能 ESP32 开发板/模组管脚 其它设备管脚 下载固件…

【Python报错】AttributeError: ‘NoneType‘ object has no attribute ‘xxx‘

成功解决“AttributeError: ‘NoneType’ object has no attribute ‘xxx’”错误的全面指南 一、引言 在Python编程中&#xff0c;AttributeError是一种常见的异常类型&#xff0c;它通常表示尝试访问对象没有的属性或方法。而当我们看到错误消息“AttributeError: ‘NoneTyp…

激发AI创新潜能,OPENAIGC开发者大赛赛题解析

人工智能&#xff08;AI&#xff09;的飞速发展&#xff0c;特别是AIGC、大模型、数字人技术的成熟&#xff0c;不仅改变了数据处理和信息消费的方式&#xff0c;也为企业和个人提供了前所未有的机遇。在这种技术进步的背景下&#xff0c;由联想拯救者、AIGC开放社区、英特尔共…

基于SSM+Jsp的高校二手交易平台

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

【远程连接服务器】—— Workbench和Xshell远程连接阿里云服务器失败和运行Xshell报错找不到 MSVCP110.d的问题分析及解决

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、远程连接不上服务器1. Workbench远程连接失败2.Xshell也连接不上3.解决方法(1)问题描述&#xff1a;(2)解决&#xff1a; 4.再次连接服务器 二、运行Xshell…

【前端面试3+1】18 vue2和vue3父传子通信的差别、props传递的数据在子组件是否可以修改、如何往window上添加自定义属性、【多数元素】

一、vue2和vue3父传子通信的差别 1、Vue2 父组件向子组件传递数据通常通过props属性来实现。父组件可以在子组件的标签中使用v-bind指令将数据传递给子组件的props属性。在子组件中&#xff0c;可以通过props属性来接收这些数据。这种方式是一种单向数据流的方式&#xff0c;父…

python-opencv图像分割

文章目录 二值化图像骨骼连通域分割 二值化 所谓图像分割&#xff0c;就是将图像的目标和背景分离开来&#xff0c;更直观一点&#xff0c;就是把目标涂成白色&#xff0c;背景涂成黑色&#xff0c;言尽于此&#xff0c;是不是恍然大悟&#xff1a;这不就是二值化么&#xff1…

香橙派 AIpro 的系统评测

0. 前言 你好&#xff0c;我是悦创。 今天受邀测评 Orange Pi AIpro开发板&#xff0c;我将准备用这个测试简单的代码来看看这块开发版的性能体验。 分别从&#xff1a;Sysbench、Stress-ng、PyPerformance、RPi.GPIO Benchmark、Geekbench 等方面来测试和分析结果。 下面就…

DevExpress Installed

一、What’s Installed 统一安装程序将DevExpress控件和库注册到Visual Studio中&#xff0c;并安装DevExpress实用工具、演示应用程序和IDE插件。 Visual Studio工具箱中的DevExpress控件 Visual Studio中的DevExpress菜单 Demo Applications 演示应用程序 Launch the Demo…

PS去水印

去除图片水印 step1&#xff1a;使用套索工具框选图片水印 step2&#xff1a;CTRLshiftU 去色 step3&#xff1a;CTRLL 色阶 step4&#xff1a;使用第三根吸管去点击需要去掉的图片水印 成功去掉 去掉文字水印 也可按照上述方法去除

计算机网络 期末复习(谢希仁版本)第1章

大众熟知的三大网络&#xff1a;电信网络、有线电视网络、计算机网络。发展最快起到核心的是计算机网络。Internet是全球最大、最重要的计算机网络。互联网&#xff1a;流行最广、事实上的标准译名。互连网&#xff1a;把许多网络通过一些路由器连接在一起。与网络相连的计算机…

【多模态】35、TinyLLaVA | 3.1B 的 LMM 模型就可以实现 7B LMM 模型的效果

文章目录 一、背景二、方法2.1 模型结构2.2 训练 pipeline 三、模型设置3.1 模型结构3.2 训练数据3.3 训练策略3.4 评测 benchmark 四、效果 论文&#xff1a;TinyLLaVA: A Framework of Small-scale Large Multimodal Models 代码&#xff1a;https://github.com/TinyLLaVA/T…

【Unity性能优化】使用多边形碰撞器网格太多,性能消耗太大了怎么办

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 专栏交流&#x1f9e7;&…

【机器学习基础】Python编程04:五个实用练习题的解析与总结

Python是一种广泛使用的高级编程语言,它在机器学习领域中的重要性主要体现在以下几个方面: 简洁易学:Python语法简洁清晰,易于学习,使得初学者能够快速上手机器学习项目。 丰富的库支持:Python拥有大量的机器学习库,如scikit-learn、TensorFlow、Keras和PyTorch等,这些…

快团团有货源的供货大团长如何给单个订单发货?

快团团团长给单个订单发货的步骤如下&#xff1a; 登录快团团商家后台&#xff1a;首先&#xff0c;你需要以团长的身份登录快团团的商家后台管理系统。 进入订单管理页面&#xff1a;登录后&#xff0c;在后台导航中找到并点击“订单管理”或类似的选项&#xff0c;进入订单列…

算法人生(19): 从“LangChain的六大组件”看“个人职业规划”

我们今天要说说和大模型有着密切关系的Langchain &#xff0c;它提供了一个平台&#xff0c;让开发者可以更加轻松地训练、部署和管理这些大模型。具体来说&#xff0c;Langchain 可以通过提供高性能的计算资源、灵活的模型管理和部署选项、以及丰富的监控和调试功能&#xff0…

企业软件产品和服务 之 设计保证安全 七项承诺

1. 引言 公司如何保护自己免受数据泄露的影响&#xff1f;标准答案就是&#xff1a; “启用多因素身份验证”——MTA&#xff08;Enable multifactor authentication&#xff09;。 但是&#xff0c;目前很多公司仍然盲目地只使用密码作为唯一的身份来源。 网络安全的核心是…

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

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

HarmonyOS(二十四)——Harmonyos通用事件之触摸事件

1.触摸事件。 触摸事件是HarmonyOS通用事件的一种事件之一&#xff0c;当手指在组件上按下、滑动、抬起时触发。 名称是否冒泡功能描述onTouch(event: (event?: TouchEvent) > void)是手指触摸动作触发该回调&#xff0c;event返回值见下面TouchEvent介绍。 2. TouchEve…

埃隆·马斯克 - 从梦想家到改变世界的企业家

埃隆马斯克 - 从梦想家到改变世界的企业家 本文内容是埃隆马斯克传的重点章节精华提炼&#xff0c;介绍了马斯克传奇一生 参考资料内容&#xff1a;埃隆马斯克传&造梦者埃隆马斯克 参考资料在文末获取&#xff0c;关注我&#xff0c;分享优质前沿资料&#xff08;IT、运…