前言

更改了计算机网络篇章,并整理更细化篇章tcp-udp独立一章了。

4.1 TCP 三次握手与四次挥手面试题 | 小林coding

TCP与UDP的区别

区别 UDP TCP
是否连接 不连接 面向连接
是否可靠 不可靠 可靠传输(传输过程中会丢失,但会重发)使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一,多对多交互通信。 仅支持一对一通信。
传输方式 面向报文 面向字节流
数据边界 保存数据边界 不保存数据边界
速度 速度快 速度慢
发送消耗 轻量级(因为 UDP 传输的信息中不承担任何间接创造连接,保证交货或秩序的信息。这也反应在包头大小。) 重量级
首部开销 首部开销小,仅8个字节 首部开销大,最小20字节,最大60字节。
有序性 不提供有序性的保证 TCP 保证了消息的有序性,即使到达客户端顺序不同,TCP 也会排序。
应用场景 IP电话,视频会议,直播,以及FPS竞技类的使用UDP帧同步。 要求可靠传输的应用例如文件传输,以及MMO类的TCP状态同步。

TCP基本格式

TCP 头格式

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。

RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

TCP建立连接-三握

为什么TCP需要三次握手,四次挥手?_tcp 为什么需要三次握手和四次挥手?-CSDN博客

img

img

LISTEN:Listening for Connections(监听连接)

SYN_SENT :Synchronize Sent(同步已发送)

SYN-RCVD :Synchronize Received 同步已接收

ESTABLISHED:Connection Established(连接已建立)

省流

第一次握手:客户端请求建立连接,向服务端发送一个同步报文(SYN=1),同时选择一个随机数 seq = x 作为初始序列号,并进入SYN_SENT状态,等待服务器确认。

第二次握手:服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文 (SYN=1,ACK=1),确认号为 ack=x+1,同时选择一个随机数seq = y 作为初始序列号,此时服务器进入SYN_RECV状态。

第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(ACK=1),确认号为 ack=y+1,序列号为 seq=x+1,客户端和服务器进入ESTABLISHED状态,完成三次握手。

关键

三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过 “确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测 “确认号(Ack)字段”,看 Ack = Seq + 1 是否成立,如果成立说明对方 正确收到了自己的数据包。

三握过程

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:

TCP 三次握手

一握

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态

第一个报文 —— SYN 报文

二握

  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。

第二个报文 —— SYN + ACK 报文

三握

  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。

第三个报文 —— ACK 报文

  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。

  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。

一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

为什么要三次握手

在这里插入图片描述

握手方式来说:

  • 如果只有一次握手,Client不能确定与Server的单向连接,更加不能确定Server与Client单向连接;
  • 如果只有两次握手,Client确定与Server的单向连接,但是Sevrer不能确定与Client的单向连接;
  • 只有三次握手,Client与Server才能相互确认双向连接,实现双方的数据传输。

原因来说:

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)

  • 三次握手才可以同步双方的初始序列号

  • 三次握手才可以避免资源浪费

阻止重复历史连接(重传RST)

首要原因是为了防止旧的重复连接初始化造成混乱。

三次握手避免历史连接

客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
  • 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
  • 服务端收到 RST 报文后,就会释放连接。
  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。

同步双方初始序列号(四握)

四次握手与三次握手

TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
  • 可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

避免资源浪费(二握)

如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?

如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

两次握手会造成资源浪费

小结

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

不使用「两次握手」和「四次握手」的原因:

「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

TCP连接断开-四挥

img

img

ESTABLISHED:Connection Established(连接已建立)
FIN_WAIT_1:Finish Wait 1(终止等待 1)
FIN_WAIT_2:Finish Wait 2(终止等待 2)
CLOSE_WAIT:Close Wait(关闭等待)
LAST_ACK:Last Acknowledgment(最后确认)
TIME_WAIT:Time Wait(时间等待)
CLOSED:Connection Closed(连接已关闭)

省流

挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:

  • 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入 FIN_WAIT_1 状态,这表示Client端没有数据要发送给Server端了。
  • 第二次挥手: Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段, ack设为seq加1, Client端进入FINWAIT_2 状态,Server端告诉Client端,我确认并同意你的关闭请求。
  • 第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
  • 第四次挥手: Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入 TIME_WAIT 状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时, Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好, Client端也可以关闭连接了。

四挥过程

客户端主动关闭连接 —— TCP 四次挥手

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
  • 你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
  • 这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么要四次挥手

在这里插入图片描述

  • 服务端在收到客户端的释放报文时,可能自己的数据报还没有发完,所以不会直接返回FIN+ACK,而只先返回一个ACK,表示自己收到了客户端的释放请求(第二次挥手)。等到服务端报文发完以后,在返回FIN(第三次挥手)。

  • 那么,我们是否可以在服务器端数据传送完成后,再返回FIN+ACK呢?中间就可以省略一次ACK了?(省略第二次挥手)

  • 试想一下,如果服务端还有很多数据需要传送,耗时长,客户端在发送释放报文后,一直没有收到反馈,那么他会认为服务端没有收到我的FIN,因此就会不停的重发FIN。(第一次挥手)

  • 所以最好的办法就是,客户端发送FIN,服务端回复ACK,表示我已经收到了,但是我在忙,你等等,我处理完成后联系你。服务端数据传送完成后,发送FIN给客户端,客户端再回复ACK。


  • 再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。
  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
  • 从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
  • 但是在特定情况下,四次挥手是可以变成三次挥手的,具体情况可以看这篇:4.22 TCP 四次挥手,可以变成三次吗? | 小林coding

四次挥手中为什么等待2MSL?

  • 确保A发送的最后一个ACK报文段能够到达B是非常重要的。这个ACK报文段有可能丢失,如果B收不到这个确认报文,就会超时重传连接释放报文段。然后A可以在2MSLMaximum Segment Lifetime,最大报文段寿命)时间内收到这个重传的连接释放报文段。接着A会重传一次确认报文,并重新启动2MSL计时器。最后,AB都进入到CLOSED状态。

  • 如果ATIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,那么它可能无法收到B重传的连接释放报文段。这样,A就不会再发送一次确认报文段,导致B无法正常进入到CLOSED状态。

  • 为了防止已失效的连接请求报文段出现在本连接中,A在发送完最后一个ACK报文段后,需要再经过2MSL的时间。这样可以确保这个连接所产生的所有报文段都从网络中消失,从而避免在下一个新的连接中出现旧的连接请求报文段。

简单来说

  1. 用来重发可能丢失(第四次挥手)的ACK报文
  2. 避免服务器有了新的数据需要发送给客户端。

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:

1
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */ 

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

TCP四种操作

图解 TCP 重传、滑动窗口、流量控制、拥塞控制 - 知乎

省流操作步骤

滑动窗口:滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法正常处理的异常。

超时重传:超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。

拥塞控制:在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时,提高性能。

流量控制:如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的数据量。流量控制与TCP协议报头中的窗口大小有关。

机制省流

1. TCP 重传

  • 定义:当发送方未收到接收方的确认(ACK)时,会重新发送数据包。
  • 触发条件
    • 超时重传:发送方在预定时间内未收到ACK。
    • 快速重传:收到三个重复ACK时,立即重传丢失的包。
  • 作用:确保数据可靠到达,即使出现丢包。

2. 滑动窗口

  • 定义:发送方和接收方通过窗口机制控制数据流量,窗口大小决定了一次能发送的数据量。
  • 工作原理
    • 发送窗口:发送方可以连续发送的数据范围。
    • 接收窗口:接收方能够处理的数据范围。
    • 滑动:随着ACK的到达,窗口向前滑动,允许发送新数据。
  • 作用:提高网络利用率,允许连续发送多个数据包。

3. 流量控制

  • 定义:通过调节发送速率,防止接收方因缓冲区不足而丢包。
  • 实现方式
    • 接收窗口:接收方通过ACK告知发送方其剩余缓冲区大小。
    • 零窗口:当接收方缓冲区满时,通知发送方暂停发送。
  • 作用:避免接收方过载,确保数据传输的稳定性。

4. 拥塞控制

  • 定义:通过调节发送速率,防止网络过载。
  • 主要算法
    • 慢启动:初始时指数增长发送窗口,探测网络容量。
    • 拥塞避免:窗口线性增长,避免过快引发拥塞。
    • 快速重传和快速恢复:通过重复ACK检测丢包,快速恢复传输。
  • 作用:防止网络拥塞,保持网络稳定。

总结

  • TCP 重传:确保数据可靠到达。
  • 滑动窗口:提高传输效率。
  • 流量控制:防止接收方过载。
  • 拥塞控制:防止网络拥塞。

这些机制共同保障了TCP的可靠性和高效性。

Socket编程

服务端: CREATE->BIND->LISTEN->ACCEPT->SEND->CLOSE

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <cstdio>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // 正确链接Winsock库

#define BUF_SIZE 1024 // 增大缓冲区尺寸
#define PORT 1234

int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed: %d\n", WSAGetLastError());
return 1;
}

// 创建服务器套接字
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serverSocket == INVALID_SOCKET) {
fprintf(stderr, "Socket creation error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}

// 配置服务器地址
sockaddr_in serverAddr = {};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 使用标准回环地址
serverAddr.sin_port = htons(PORT);

// 绑定套接字
if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
fprintf(stderr, "Bind failed: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}

// 开始监听
if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
fprintf(stderr, "Listen failed: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}

printf("Server listening on port %d...\n", PORT);

// 接受客户端连接
sockaddr_in clientAddr = {};
int clientAddrLen = sizeof(clientAddr);
SOCKET clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (clientSocket == INVALID_SOCKET) {
fprintf(stderr, "Accept failed: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}

// 网络通信处理
char buffer[BUF_SIZE];
int bytesReceived;
while ((bytesReceived = recv(clientSocket, buffer, BUF_SIZE, 0)) > 0) {
// 安全发送数据(处理部分发送情况)
int totalSent = 0;
while (totalSent < bytesReceived) {
int bytesSent = send(clientSocket, buffer + totalSent,
bytesReceived - totalSent, 0);
if (bytesSent <= 0) break;
totalSent += bytesSent;
}
}

// 错误处理
if (bytesReceived == SOCKET_ERROR) {
fprintf(stderr, "Receive error: %d\n", WSAGetLastError());
}

// 清理资源
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}

客户端: SOCKET->CONNECT->RECV->CLOSE

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <cstdio>
#include <cstdlib>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 1024 // 增大缓冲区防止溢出
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 1234

int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed: %d\n", WSAGetLastError());
return EXIT_FAILURE;
}

// 创建客户端套接字
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET) {
fprintf(stderr, "Socket creation error: %d\n", WSAGetLastError());
WSACleanup();
return EXIT_FAILURE;
}

// 配置服务器地址
sockaddr_in serverAddr = {};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(SERVER_PORT);

// 建立连接
if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
fprintf(stderr, "Connection failed: %d\n", WSAGetLastError());
closesocket(clientSocket);
WSACleanup();
return EXIT_FAILURE;
}

printf("Connected to %s:%d\n", SERVER_IP, SERVER_PORT);

// 安全输入处理
char buffer[BUF_SIZE] = {0};
printf("Input a string (max %d chars): ", BUF_SIZE-1);
if (fgets(buffer, BUF_SIZE, stdin) == NULL) {
fprintf(stderr, "Input error\n");
closesocket(clientSocket);
WSACleanup();
return EXIT_FAILURE;
}

// 移除换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[--len] = '\0';
}

// 安全发送数据
int bytesSent = send(clientSocket, buffer, len, 0);
if (bytesSent == SOCKET_ERROR) {
fprintf(stderr, "Send failed: %d\n", WSAGetLastError());
closesocket(clientSocket);
WSACleanup();
return EXIT_FAILURE;
}

// 接收响应数据
char recvBuffer[BUF_SIZE] = {0};
int bytesReceived = recv(clientSocket, recvBuffer, BUF_SIZE-1, 0);
if (bytesReceived == SOCKET_ERROR) {
fprintf(stderr, "Receive failed: %d\n", WSAGetLastError());
} else if (bytesReceived == 0) {
printf("Connection closed by server\n");
} else {
recvBuffer[bytesReceived] = '\0'; // 确保字符串终止
printf("Server response: %s\n", recvBuffer);
}

// 清理资源
closesocket(clientSocket);
WSACleanup();
return EXIT_SUCCESS;
}