- Socket接口
注:以下为本人实验报告原文,切勿直接copy
实验报告采用C++
代码分享项目:https://github.com/buyun14/PoCN
(与blog使用代码多数不同)
Socket接口
1. 实验目的
- 了解网络进程间通信的基本原理。
- 掌握Client/Server模式的工作机制。
- 学习Socket编程接口及其在TCP与UDP协议下的应用。
2. 实验环境
- 操作系统:Windows/Linux
- 编程语言:C/C++/python/Java/node.js/Go等等
- 开发工具:[如Visual Studio 2022,pycharm,VS Code, GCC等]
3. 实验原理
3.1 网络进程间通信
- 描述网络进程间通信的基本概念。
- 介绍进程标识的方法:在同一台主机上通过进程ID标识,网络上通过(主机IP地址,端口号)标识。
3.2 Client/Server模式
- 描述Client/Server模式的基本工作流程。
Client/Server (C/S) 模式是一种常见的网络通信模型,其中客户端(Client)和服务端(Server)通过网络进行交互。客户端主动发起服务请求,服务端被动等待并响应这些请求。以下是C/S模式的基本工作流程:
1. 服务端初始化
创建套接字(Socket)
- 服务端首先调用
socket()
函数创建一个套接字,用于监听客户端的连接请求。 - 例如:
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
- 服务端首先调用
绑定地址信息(Bind)
- 使用
bind()
函数将套接字绑定到特定的本地地址和端口。 - 例如:
bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
- 其中,
serverAddr
是一个包含本地IP地址和端口号的sockaddr_in
结构体。
- 使用
监听连接请求(Listen)
- 调用
listen()
函数使套接字进入监听状态,等待客户端的连接请求。 - 例如:
listen(serverSocket, backlog);
backlog
参数指定了连接请求队列的最大长度。
- 调用
接受连接请求(Accept)
- 使用
accept()
函数接受客户端的连接请求,建立一个新的套接字用于与客户端通信。 - 例如:
SOCKET clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &clientAddrLen);
clientSocket
是与特定客户端通信的新套接字,clientAddr
包含客户端的地址信息。
- 使用
2. 客户端初始化
创建套接字(Socket)
- 客户端调用
socket()
函数创建一个套接字,用于与服务端进行通信。 - 例如:
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
- 客户端调用
连接服务端(Connect)
- 使用
connect()
函数向服务端发起连接请求。 - 例如:
connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
serverAddr
是一个包含服务端IP地址和端口号的sockaddr_in
结构体。
- 使用
3. 数据传输
客户端发送数据
- 客户端使用
send()
函数向服务端发送数据。 - 例如:
send(clientSocket, sendData, sendDataSize, 0);
- 客户端使用
服务端接收数据
- 服务端使用
recv()
函数接收客户端发送的数据。 - 例如:
recv(clientSocket, receiveData, receiveDataSize, 0);
- 服务端使用
服务端发送数据
- 服务端使用
send()
函数向客户端发送数据作为响应。 - 例如:
send(clientSocket, responseData, responseSize, 0);
- 服务端使用
客户端接收数据
- 客户端使用
recv()
函数接收服务端发送的数据。 - 例如:
recv(clientSocket, receiveResponse, receiveResponseSize, 0);
- 客户端使用
4. 断开连接
- 关闭套接字(Close)
- 客户端和服务端在通信结束后,分别调用
closesocket()
函数关闭套接字,释放资源。 - 例如:
closesocket(clientSocket);
和closesocket(serverSocket);
- 客户端和服务端在通信结束后,分别调用
- 解释TCP和UDP协议的区别,特别是在建立连接、数据传输可靠性和系统资源需求方面的差异。
TCP(传输控制协议)和UDP(用户数据报协议)都是Internet协议套件的一部分,用于在网络中传输数据。它们在设计上有着根本的不同,适用于不同类型的网络通信需求。以下是TCP与UDP在建立连接、数据传输可靠性以及系统资源需求方面的主要区别:
建立连接
TCP 是一种面向连接的协议。在两个设备之间开始数据交换之前,必须先建立一个可靠的连接。这个过程通常涉及到三次握手(SYN, SYN-ACK, ACK),确保双方都准备好接收数据。这种机制保证了数据传输前的连接可靠性。
UDP 是一种无连接的协议。它不需要在发送数据包之前建立连接。这意味着发送方可以立即开始发送数据,而无需等待接收方的确认或准备状态。这使得UDP具有更低的延迟,但同时也缺乏TCP的连接可靠性。
数据传输可靠性
TCP 提供了高度的数据传输可靠性。它通过序列号、确认应答(ACK)、重传丢失的数据包等机制来确保数据的完整性和顺序。如果数据包在网络中丢失,TCP会自动请求重新发送这些数据包。此外,TCP还提供了流量控制和拥塞控制功能,以避免网络过载。
UDP 不提供数据传输的可靠性保证。它不会检查数据包是否成功到达目的地,也不会重传丢失的数据包。因此,使用UDP时可能会出现数据包丢失或乱序的情况。然而,对于某些应用来说,如在线视频直播或语音通话,即使有轻微的数据丢失,也能接受,因为这些应用更关注实时性而非绝对的可靠性。
系统资源需求
TCP 因为其连接建立、数据确认、重传机制以及流控和拥塞控制等功能,通常需要更多的系统资源(例如内存和CPU)。每个TCP连接都会占用一定的内核资源,当大量并发连接存在时,可能会对服务器性能产生影响。
UDP 相对而言对系统资源的需求较低。由于UDP没有复杂的握手过程和数据确认机制,它的开销较小。这对于需要处理大量简单请求的应用(如DNS查询)来说是一个优点。
总结来说,TCP更适合那些需要高可靠性的应用,如文件传输、网页浏览等;而UDP则适用于对实时性要求较高且能容忍一定数据丢失的应用,比如在线游戏、视频会议等。选择哪种协议取决于具体应用场景的需求。
3.3 Socket编程接口
- 介绍Socket编程接口的主要API函数,包括socket(), bind(), listen(), accept(), connect(), send(), recv(), sendto(), recvfrom(), closesocket()等。
Socket编程接口的主要API函数
Socket编程接口提供了一系列函数,用于在网络中进行进程间的通信。以下是这些主要API函数的详细介绍:
1. socket()
- 功能:创建一个套接字。
- 原型:
SOCKET socket(int af, int type, int protocol);
- 参数:
af
:地址族,通常为AF_INET
(IPv4)或AF_INET6
(IPv6)。type
:套接字类型,常见类型有SOCK_STREAM
(面向连接的TCP)和SOCK_DGRAM
(无连接的UDP)。protocol
:协议类型,通常为IPPROTO_TCP
或IPPROTO_UDP
。
- 返回值:成功返回一个套接字描述符(非负整数),失败返回
INVALID_SOCKET
。
2. bind()
- 功能:将套接字绑定到本地地址和端口。
- 原型:
int bind(SOCKET s, const struct sockaddr *name, int namelen);
- 参数:
s
:要绑定的套接字描述符。name
:指向包含本地地址和端口的sockaddr
结构体的指针。namelen
:sockaddr
结构体的大小。
- 返回值:成功返回0,失败返回
SOCKET_ERROR
。
3. listen()
- 功能:将套接字设置为监听状态,准备接收连接请求。
- 原型:
int listen(SOCKET s, int backlog);
- 参数:
s
:要监听的套接字描述符。backlog
:连接请求队列的最大长度。
- 返回值:成功返回0,失败返回
SOCKET_ERROR
。
4. accept()
- 功能:接受一个连接请求,创建一个新的套接字用于与客户端通信。
- 原型:
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
- 参数:
s
:监听套接字描述符。addr
:指向存储客户端地址信息的sockaddr
结构体的指针。addrlen
:sockaddr
结构体的大小。
- 返回值:成功返回一个新的套接字描述符,失败返回
INVALID_SOCKET
。
5. connect()
- 功能:向服务端发起连接请求。
- 原型:
int connect(SOCKET s, const struct sockaddr *name, int namelen);
- 参数:
s
:要连接的套接字描述符。name
:指向包含服务端地址和端口的sockaddr
结构体的指针。namelen
:sockaddr
结构体的大小。
- 返回值:成功返回0,失败返回
SOCKET_ERROR
。
6. send()
- 功能:向指定的套接字发送数据。
- 原型:
int send(SOCKET s, const char *buf, int len, int flags);
- 参数:
s
:要发送数据的套接字描述符。buf
:指向要发送的数据缓冲区的指针。len
:要发送的数据长度。flags
:发送标志,通常为0。
- 返回值:成功返回实际发送的字节数,失败返回
SOCKET_ERROR
。
7. recv()
- 功能:从指定的套接字接收数据。
- 原型:
int recv(SOCKET s, char *buf, int len, int flags);
- 参数:
s
:要接收数据的套接字描述符。buf
:指向接收数据缓冲区的指针。len
:缓冲区的大小。flags
:接收标志,通常为0。
- 返回值:成功返回实际接收到的字节数,失败返回
SOCKET_ERROR
,如果连接被关闭则返回0。
8. sendto()
- 功能:向指定的地址发送数据(用于UDP)。
- 原型:
int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
- 参数:
s
:要发送数据的套接字描述符。buf
:指向要发送的数据缓冲区的指针。len
:要发送的数据长度。flags
:发送标志,通常为0。to
:指向目标地址的sockaddr
结构体的指针。tolen
:sockaddr
结构体的大小。
- 返回值:成功返回实际发送的字节数,失败返回
SOCKET_ERROR
。
9. recvfrom()
- 功能:从指定的地址接收数据(用于UDP)。
- 原型:
int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);
- 参数:
s
:要接收数据的套接字描述符。buf
:指向接收数据缓冲区的指针。len
:缓冲区的大小。flags
:接收标志,通常为0。from
:指向存储发送方地址的sockaddr
结构体的指针。fromlen
:sockaddr
结构体的大小。
- 返回值:成功返回实际接收到的字节数,失败返回
SOCKET_ERROR
。
10. closesocket()
- 功能:关闭套接字,释放相关资源。
- 原型:
int closesocket(SOCKET s);
- 参数:
s
:要关闭的套接字描述符。
- 返回值:成功返回0,失败返回
SOCKET_ERROR
。
- 说明TCP与UDP在Socket编程中的不同之处。
TCP与UDP在Socket编程中的不同之处
TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的传输层协议,它们在Socket编程中有许多不同之处。以下是一些主要的区别:
1. 连接方式
- TCP:面向连接的协议。在数据传输之前,客户端和服务器之间需要通过三次握手建立连接。数据传输完成后,通过四次挥手断开连接。
- UDP:无连接的协议。数据传输前不需要建立连接,每个数据包独立发送,接收端也不需要确认。
2. 数据传输可靠性
- TCP:提供可靠的数据传输。数据包按顺序传输,确保数据的完整性和顺序性。如果数据包丢失或损坏,TCP会自动重传。
- UDP:不保证数据传输的可靠性。数据包可能丢失、重复或乱序到达。UDP不进行重传,也不保证数据的顺序。
3. 数据传输模式
- TCP:采用字节流模式。数据被视为一个连续的字节流,没有边界。接收端需要自行处理数据的分段和重组。
- UDP:采用数据报模式。每个数据包都有明确的边界,发送和接收的数据包保持一致。
4. 系统资源需求
- TCP:需要更多的系统资源。维护连接状态、重传机制和拥塞控制等都需要额外的资源。
- UDP:对系统资源的需求较少。由于没有连接状态和重传机制,UDP的开销较低。
5. 系统函数调用
TCP:
- 创建套接字:
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- 绑定地址:
bind(socket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
- 监听连接:
listen(socket, backlog);
- 接受连接:
accept(socket, (struct sockaddr *)&clientAddr, &clientAddrLen);
- 连接服务端:
connect(socket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
- 发送数据:
send(socket, buffer, length, flags);
- 接收数据:
recv(socket, buffer, length, flags);
- 关闭套接字:
closesocket(socket);
- 创建套接字:
UDP:
- 创建套接字:
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
- 绑定地址:
bind(socket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
- 发送数据:
sendto(socket, buffer, length, flags, (struct sockaddr *)&destAddr, sizeof(destAddr));
- 接收数据:
recvfrom(socket, buffer, length, flags, (struct sockaddr *)&srcAddr, &srcAddrLen);
- 关闭套接字:
closesocket(socket);
- 创建套接字:
6. 客户端和服务端的识别
- TCP:服务器通过
accept()
函数获取客户端的地址信息,客户端的地址信息在连接建立时由操作系统提供。 - UDP:每次发送数据时,客户端需要指定目标地址信息。服务器通过
recvfrom()
函数获取发送方的地址信息。
7. 适用场景
- TCP:适用于需要高可靠性的应用场景,如文件传输、Web服务、电子邮件等。
- UDP:适用于对实时性要求较高且可以容忍一定数据丢失的应用场景,如视频流、在线游戏、VoIP等。
4. 实验内容与步骤
4.1 基于TCP协议的通信编程
- 单向通信
- 双向通信
鉴于单向通信即双向通信的阉割实现,少了服务端回复客户端的过程,因此只需要实现双向通信即可。
1 | //server |
- 多媒体文件传输:
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315// server_file
std::mutex file_mutex;
void handle_client(int connfd, const struct sockaddr_in& clientAddr) {
char clientIP[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP)) == nullptr) {
std::cerr << "inet_ntop failed" << std::endl;
closesocket(connfd);
return;
}
std::cout << "Connected to client IP: " << clientIP << ", Port: " << ntohs(clientAddr.sin_port) << std::endl;
std::ofstream file(FILE_NAME, std::ios::binary | std::ios::ate);
// 接收文件大小
size_t file_size;
if (recv(connfd, reinterpret_cast<char*>(&file_size), sizeof(file_size), 0) < 0) {
std::cerr << "Failed to receive file size" << std::endl;
closesocket(connfd);
return;
}
std::cout << "File size: " << file_size << " bytes" << std::endl;
char buffer[BUFFER_SIZE] = { 0 };
int n;
size_t total_received = 0;
while (total_received < file_size) {
size_t start, end;
if (recv(connfd, reinterpret_cast<char*>(&start), sizeof(start), 0) < 0) {
std::cerr << "Failed to receive start position" << std::endl;
break;
}
if (recv(connfd, reinterpret_cast<char*>(&end), sizeof(end), 0) < 0) {
std::cerr << "Failed to receive end position" << std::endl;
break;
}
size_t chunk_received = 0;
while (chunk_received < (end - start) && (n = recv(connfd, buffer, sizeof(buffer), 0)) > 0) {
file_mutex.lock();
file.seekp(start + chunk_received);
file.write(buffer, n);
file_mutex.unlock();
chunk_received += n;
total_received += n;
std::cout << "已接收: " << total_received << " 字节" << std::endl;
// 发送确认消息
char ack[] = "OK";
if (send(connfd, ack, 2, 0) < 0) {
std::cerr << "Ack send failed" << std::endl;
break;
}
}
if (n < 0) {
std::cerr << "Receive failed" << std::endl;
break;
}
}
if (total_received < file_size) {
std::cerr << "File transfer incomplete" << std::endl;
}
file.close();
closesocket(connfd);
}
int main() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
if (bind(listenfd, (const struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Bind failed" << std::endl;
closesocket(listenfd);
WSACleanup();
return -1;
}
if (listen(listenfd, 5) < 0) {
std::cerr << "Listen failed" << std::endl;
closesocket(listenfd);
WSACleanup();
return -1;
}
std::cout << "Server listening on port " << PORT << std::endl;
while (true) {
struct sockaddr_in clientAddr;
socklen_t addr_len = sizeof(clientAddr);
int connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &addr_len);
if (connfd < 0) {
std::cerr << "Accept failed" << std::endl;
continue;
}
// 创建新线程处理客户端连接
std::thread client_thread(handle_client, connfd, clientAddr);
client_thread.detach(); // 分离线程,允许其独立运行
}
closesocket(listenfd);
WSACleanup();
return 0;
}
// client_file
std::mutex mtx;
void send_chunk(int sockfd, std::ifstream& file, size_t start, size_t end) {
char buffer[BUFFER_SIZE] = { 0 };
// 发送起始位置和结束位置
if (send(sockfd, reinterpret_cast<const char*>(&start), sizeof(start), 0) < 0) {
std::cerr << "Failed to send start position" << std::endl;
return;
}
if (send(sockfd, reinterpret_cast<const char*>(&end), sizeof(end), 0) < 0) {
std::cerr << "Failed to send end position" << std::endl;
return;
}
file.seekg(start);
while (file.tellg() < end) {
file.read(buffer, sizeof(buffer));
size_t bytes_read = file.gcount();
if (bytes_read == 0) {
break;
}
int sent_bytes = send(sockfd, buffer, bytes_read, 0);
if (sent_bytes < 0) {
std::cerr << "Send failed" << std::endl;
break;
}
// 等待服务器确认
char ack[2] = { 0 };
int retries = 0;
while (retries < MAX_RETRIES) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
struct timeval timeout;
timeout.tv_sec = TIMEOUT_MS / 1000;
timeout.tv_usec = (TIMEOUT_MS % 1000) * 1000;
int select_ret = select(sockfd + 1, &readfds, nullptr, nullptr, &timeout);
if (select_ret < 0) {
std::cerr << "Select failed" << std::endl;
break;
}
else if (select_ret == 0) {
std::cerr << "Timeout waiting for acknowledgment, retrying... (" << retries + 1 << "/" << MAX_RETRIES << ")" << std::endl;
retries++;
continue;
}
else {
if (recv(sockfd, ack, 2, 0) <= 0) {
std::cerr << "Ack receive failed, retrying... (" << retries + 1 << "/" << MAX_RETRIES << ")" << std::endl;
retries++;
continue;
}
break;
}
}
if (retries >= MAX_RETRIES) {
std::cerr << "Max retries reached, giving up." << std::endl;
break;
}
}
}
int main() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
if (connect(sockfd, (const struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Connect failed" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
std::cout << "Connected to server" << std::endl;
std::ifstream file("C:\\Users\\21354\\Videos\\asdf.mp4", std::ios::binary);
if (!file) {
std::cerr << "无法打开文件" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
file.seekg(0, std::ios::end);
size_t file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::cout << "文件大小: " << file_size << " 字节" << std::endl;
// 发送文件大小
if (send(sockfd, reinterpret_cast<const char*>(&file_size), sizeof(file_size), 0) < 0) {
std::cerr << "Failed to send file size" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
size_t num_threads = 1; // 可以根据需要调整线程数量
size_t chunk_size = (file_size + num_threads - 1) / num_threads;
std::vector<std::thread> threads;
for (size_t i = 0; i < num_threads; ++i) {
size_t start = i * chunk_size;
size_t end = (i + 1) * chunk_size;
if (i == num_threads - 1) {
end = file_size;
}
threads.emplace_back(send_chunk, sockfd, std::ref(file), start, end);
}
for (auto& thread : threads) {
thread.join();
}
std::string end_message = "exit";
send(sockfd, end_message.c_str(), end_message.length(), 0);
file.close();
closesocket(sockfd);
WSACleanup();
return 0;
}
4.2 基于UDP协议的通信编程
- 单向通信:
- 双向通信:
鉴于单向通信即双向通信的阉割实现,少了服务端回复客户端的过程,因此只需要实现双向通信即可。
注:winsock2.h
和ws2tcpip.h
是 Windows 特有的头文件,专门用于 Windows 平台上的网络编程。这些头文件提供了 Windows Sockets API(Winsock API)的相关定义和函数声明。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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165//server
int main() {
WSADATA wsaData;
int result;
// 初始化Winsock
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int sockfd; // 套接字描述符
char buffer[BUFFER_SIZE]; // 接收缓冲区
struct sockaddr_in serverAddr, clientAddr; // 服务器和客户端的地址结构体
int addr_len = sizeof(clientAddr); // 地址结构体的长度
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr)); // 清零地址结构体
serverAddr.sin_family = AF_INET; // 使用IPv4地址族
serverAddr.sin_addr.s_addr = INADDR_ANY; // 通配IP地址
serverAddr.sin_port = htons(PORT); // 设置端口号
// 绑定套接字到指定地址
if (bind(sockfd, (const struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Bind failed" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
std::cout << "UDP服务器启动,正在监听端口 " << PORT << "..." << std::endl;
while (true) {
// 接收来自客户端的消息
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&clientAddr, &addr_len);
buffer[n] = '\0'; // 添加字符串终止符
// 使用inet_ntop转换IP地址
char clientIP[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP)) == nullptr) {
std::cerr << "inet_ntop failed" << std::endl;
continue;
}
std::cout << "收到IP地址为 " << clientIP << " 端口号为 " << ntohs(clientAddr.sin_port) << " 发送来的内容: " << buffer << std::endl;
// 向客户端发送应答
std::string response = "消息已收到";
sendto(sockfd, response.c_str(), response.size(), 0, (const struct sockaddr*)&clientAddr, addr_len);
// 打印客户端IP地址和端口号
std::cout << "发送应答至 IP地址 " << clientIP << " 端口号 " << ntohs(clientAddr.sin_port) << std::endl;
}
// 关闭套接字
closesocket(sockfd);
WSACleanup(); // 清理Winsock
return 0;
}
//client
int main() {
WSADATA wsaData;
int result;
// 初始化Winsock
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int sockfd; // 套接字描述符
char buffer[BUFFER_SIZE]; // 发送和接收缓冲区
struct sockaddr_in serverAddr; // 服务器地址结构体
int addr_len = sizeof(serverAddr); // 地址结构体的长度
int n; // 接收消息的长度
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr)); // 清零地址结构体
serverAddr.sin_family = AF_INET; // 使用IPv4地址族
serverAddr.sin_port = htons(PORT); // 设置端口号
// 使用inet_pton转换IP地址
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
int N = 0; // 消息计数器
while (true) {
// 从键盘读取输入
std::cout << "请输入要发送的消息(输入exit退出): ";
std::cin.getline(buffer, BUFFER_SIZE);
// 如果输入的是exit,则退出
if (std::string(buffer) == "exit" || std::string(buffer) == "EXIT") {
break;
}
// 发送消息到服务器
sendto(sockfd, buffer, strlen(buffer), 0, (const struct sockaddr*)&serverAddr, addr_len);
std::cout << "已发送消息: " << buffer << std::endl;
// 接收服务器的应答
n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&serverAddr, &addr_len);
buffer[n] = '\0'; // 添加字符串终止符
// 使用inet_ntop转换IP地址
char serverIP[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &serverAddr.sin_addr, serverIP, sizeof(serverIP)) == nullptr) {
std::cerr << "inet_ntop failed" << std::endl;
continue;
}
std::cout << "收到IP地址为 " << serverIP << " 端口号为 " << ntohs(serverAddr.sin_port) << " 发送来的应答: " << buffer << std::endl;
N++;
}
// 关闭套接字
closesocket(sockfd);
WSACleanup(); // 清理Winsock
return 0;
} - 多媒体文件传输:
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224//server_file
struct Packet {
int seq_num; // 序号
char data[BUFFER_SIZE]; // 数据
};
int main() {
WSADATA wsaData;
int result;
// 初始化Winsock
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int sockfd; // 套接字描述符
struct sockaddr_in serverAddr, clientAddr; // 服务器和客户端的地址结构体
int addr_len = sizeof(clientAddr); // 地址结构体的长度
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr)); // 清零地址结构体
serverAddr.sin_family = AF_INET; // 使用IPv4地址族
serverAddr.sin_addr.s_addr = INADDR_ANY; // 通配IP地址
serverAddr.sin_port = htons(PORT); // 设置端口号
// 绑定套接字到指定地址
if (bind(sockfd, (const struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Bind failed" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
std::cout << "UDP服务器启动,正在监听端口 " << PORT << "..." << std::endl;
std::ofstream file(FILE_NAME, std::ios::binary);
while (true) {
Packet packet;
int n = recvfrom(sockfd, (char*)&packet, sizeof(packet), 0, (struct sockaddr*)&clientAddr, &addr_len);
if (n < 0) {
std::cerr << "Receive failed" << std::endl;
continue;
}
// 使用inet_ntop转换IP地址
char clientIP[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP)) == nullptr) {
std::cerr << "inet_ntop failed" << std::endl;
continue;
}
std::cout << "收到IP地址为 " << clientIP << " 端口号为 " << ntohs(clientAddr.sin_port) << " 序号为 " << packet.seq_num << " 的数据包" << std::endl;
// 写入文件
file.write(packet.data, n - sizeof(int));
// 发送确认
Packet ack;
ack.seq_num = packet.seq_num;
sendto(sockfd, (const char*)&ack, sizeof(ack), 0, (const struct sockaddr*)&clientAddr, addr_len);
std::cout << "发送确认序号为 " << ack.seq_num << " 的确认消息" << std::endl;
}
// 关闭文件
file.close();
// 关闭套接字
closesocket(sockfd);
WSACleanup(); // 清理Winsock
return 0;
}
//client_file
struct Packet {
int seq_num; // 序号
char data[BUFFER_SIZE]; // 数据
};
int main() {
WSADATA wsaData;
int result;
// 初始化Winsock
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed: " << result << std::endl;
return -1;
}
int sockfd; // 套接字描述符
struct sockaddr_in serverAddr; // 服务器地址结构体
int addr_len = sizeof(serverAddr); // 地址结构体的长度
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "Socket creation failed" << std::endl;
WSACleanup();
return -1;
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr)); // 清零地址结构体
serverAddr.sin_family = AF_INET; // 使用IPv4地址族
serverAddr.sin_port = htons(PORT); // 设置端口号
// 使用inet_pton转换IP地址
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
//std::ifstream file("file_to_send.mp4", std::ios::binary);
std::ifstream file("D:\\Users\\21354\\Videos\\无标题视频 - Screen Recording - 2023_9_8 23_11_24.webm", std::ios::binary);
if (!file) {
std::cerr << "无法打开文件" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
file.seekg(0, std::ios::end);
size_t file_size = file.tellg();
file.seekg(0, std::ios::beg);
int total_packets = (file_size + BUFFER_SIZE - 1) / BUFFER_SIZE;
std::cout << "文件大小: " << file_size << " 字节,总共 " << total_packets << " 个数据包" << std::endl;
for (int i = 0; i < total_packets; ++i) {
Packet packet;
packet.seq_num = i;
file.read(packet.data, BUFFER_SIZE);// 读取数据包
size_t bytes_read = file.gcount();// 读取的字节数
sendto(sockfd, (const char*)&packet, bytes_read + sizeof(int), 0, (const struct sockaddr*)&serverAddr, addr_len);// 发送数据包
std::cout << "发送序号为 " << packet.seq_num << " 的数据包" << std::endl;
// 等待确认
Packet ack;
bool received_ack = false;// 确认消息是否已收到
auto start_time = std::chrono::steady_clock::now();// 开始时间
while (!received_ack) {
fd_set readfds;
FD_ZERO(&readfds);// 定义文件描述符集
FD_SET(sockfd, &readfds);// 将套接字加入文件描述符集
timeval timeout;// 定义超时时间
timeout.tv_sec = TIMEOUT_MS / 1000;
timeout.tv_usec = (TIMEOUT_MS % 1000) * 1000;
int select_ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);// 等待套接字可读
if (select_ret > 0 && FD_ISSET(sockfd, &readfds)) {
int n = recvfrom(sockfd, (char*)&ack, sizeof(ack), 0, (struct sockaddr*)&serverAddr, &addr_len);
if (n >= 0 && ack.seq_num == i) {
received_ack = true;
std::cout << "收到序号为 " << ack.seq_num << " 的确认消息" << std::endl;
}
}
else {
// 超时,重新发送数据包
std::cout << "超时,重新发送序号为 " << packet.seq_num << " 的数据包" << std::endl;
sendto(sockfd, (const char*)&packet, bytes_read + sizeof(int), 0, (const struct sockaddr*)&serverAddr, addr_len);
}
// 模拟网络中断
if (i == 10) { // 在第11个数据包时模拟网络中断
std::this_thread::sleep_for(std::chrono::seconds(5));
}
}
}
// 关闭文件
file.close();
// 关闭套接字
closesocket(sockfd);
WSACleanup(); // 清理Winsock
return 0;
}
5. 实验结果与分析
- 分别展示TCP和UDP协议下实验的结果截图或代码片段。
TCP通信:
UDP通信: - 对比分析两种协议下的通信效果,特别是关注数据传输的可靠性和效率。
对比两种传输方式,在视频文件的传输中,我们可以发现,UDP的传输效率要高于TCP的传输效率(之后我尝试设计了TCP的多线程实现)。(速度更快)但由于UDP传输方式不可靠,在使用UDP传输时出现了丢包、乱序等问题,最终收到的视频文件出现花屏,部分绿屏,甚至损坏无法播放等问题。
为了解决UDP传输方式的不可靠性问题,我在设计中加入序列号和确认机制,在收到确认消息后才认为数据包已收到,从而保证数据传输的可靠性。(此处在应用层实现,原理部分参照了在传输层的TCP的可靠性实现。)
6. 总结与体会
- 总结实验中遇到的问题及解决方法。
- 在udp传输文件的实验中,由于udp传输的不可靠性导致文件损坏等等情况的出现。
解决方案:协议本身的不可靠性,实验中属于正常现象。为了保证可靠传输,我在应用层程序设计中仿照tcp的部分原理,将数据包的开头替换插入为序列号,每次发送数据包等收到回复确认之后再进行发送,保证了数据的可靠传输。问题成功解决。 - 在设计多线程的tcp传输提高大文件的传输效率时,由于能力有限,设计出的服务端无法处理单个连接建立的套接字当中收到的多线程发送的文件数据包,导致文件损坏。暂未找到解决方法。但也因此尝试了md5校验等等保证和检查文件完整性的方法略有其他收获。
7. 附录
- 包含完整的实验代码。
- 提交报告所需的所有附件资料。