《TCP/IP网络编程》学习笔记 | Chapter 7:优雅地断开套接字连接
- 《TCP/IP网络编程》学习笔记 | Chapter 7:优雅地断开套接字连接
- 基于 TCP 的半关闭
- 单方面断开连接带来的问题
- 套接字和流
- 针对优雅断开的 shutdown 函数
- 为何需要半关闭?
- 基于半关闭的文件传输程序
- 基于 Windows 的实现
- Windows 下的 shutdown 函数
- 基于 Windows 的半关闭文件传输程序
- 习题
- (1)解释 TCP 中 “流” 的概念。UDP 中能否形成流?请说明原因。
- (2)Linux 中的 close 函数或 Windows 中的closesocket函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情况下会出现问题?
- (3)什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么信息?
《TCP/IP网络编程》学习笔记 | Chapter 7:优雅地断开套接字连接
基于 TCP 的半关闭
单方面断开连接带来的问题
Linux 中的 close 与 Windows 中的 closesocket 函数意味着完全断开连接,完全断开后,套接字既无法传输数据,也无法接收数据。
如上图所示,主机A断开连接后,再也无法接受主机B传输的数据,最终主机B传输的数据只能销毁。
为了解决这一问题,在关闭连接时,只关闭流的一部分(半关闭),即可以传输数据但不能接受数据,或者可以接收数据但不能传输数据。
套接字和流
一旦两台主机建立了套接字连接,每个主机就会拥有单独的输入流与输出流。一个主机的输入流与另一台主机的输出流相连,输出流与另一台主机的输入流相连。
Linux 中的 close 与 Windows 中的 closesocket 函数将同时断开这两个流。
针对优雅断开的 shutdown 函数
半关闭函数:
#include <sys/socket.h>
int shutdown(int sock, int howto);
成功时返回 0,失败时返回 -1。
参数:
- sock:需要断开的套接字文件描述符。
- howto:断开方式信息。其有 3 种可能值。SHUT_RD:断开输入流;SHUT_WR:断开输出流;SHUT_RDWR:同时断开 I/O 流。
SHUT_RD,SHUT_WR,SHUT_RDWR 的值按序分别是 0,1,2。若向 shutdown 的第二个参数传递SHUT_RD,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用输入相关函数。如果向 shutdown 的第二个参数传递SHUT_WR,则中断输出流,也就无法传输数据。若如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字SHUT_RDWR,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以SHUT_RD为参数,另一次以SHUT_WR为参数。
为何需要半关闭?
- 数据传输完成: 当一方已经发送完所有需要发送的数据,但仍然需要接收对方的响应或数据时,可以使用半关闭。这样,发送方可以告诉对方已经没有更多的数据要发送了。
- 错误处理: 如果一方在通信过程中遇到错误,它可能会选择关闭发送方向,以防止发送更多的数据,同时仍然监听对方可能发送的错误响应或状态信息。
- 保持连接: 在某些应用场景中,即使数据传输已经完成,一方可能仍希望保持连接,以便在将来需要时重新使用,而不是重新建立连接。
- 优雅地关闭连接: 在TCP连接中,半关闭允许一方在发送完所有数据后优雅地关闭连接,而不是突然断开,这有助于另一方正确地处理连接的关闭。
- 调试和诊断: 在调试网络应用程序时,半关闭可以帮助开发者理解数据流和连接状态,从而更容易地诊断问题。
比如服务器给客户端发数据,发完后客户端回一个 “Thank you”,但客户端不知道什么时候发完,所以需要一直调用 read() 函数。
改进1:可以在发完数据后服务器向客户端发送EOF表示发送结束。
问题:服务器调用 close() 函数关闭连接并发送 EOF 后,输入流也断了,客户端发的 “Thank you” 将无法收到。
改进2:调用 shutdown() 函数只关闭服务器的输入流就行了。
基于半关闭的文件传输程序
协议示意图:
服务器端的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30;
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sd, clnt_sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_sz;
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
fp = fopen("file_server.c", "rb");
serv_sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(serv_sd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(serv_sd, 5);
clnt_addr_sz = sizeof(clnt_addr);
clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_addr, &clnt_addr_sz);
while (1)
{
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
shutdown(clnt_sd, SHUT_WR);
read(clnt_sd, buf, BUF_SIZE);
printf("Message from client: %s \n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
fp = fopen("receive.dat", "wb");
sd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
connect(sd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0)
fwrite((void *)buf, 1, read_cnt, fp);
puts("Received file data");
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
基于 Windows 的实现
Windows 下的 shutdown 函数
#include <winsock2.h>
int shutdown(SOCKET s, int howto);
成功时返回 0,失败时返回 SOCKET_ERROR。
参数:
- s:要断开的套接字的句柄。
- howto:断开方式信息。其有 3 种可能值。SHUT_RECEIVE:断开输入流;SHUT_SEND:断开输出流;SHUT_BOTH:同时断开 I/O 流。其值按序分别是 0,1,2。
基于 Windows 的半关闭文件传输程序
file_server_win.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET serverSock, clientSock;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrSize;
int read_cnt;
char file_name[] = "file_server_win.c";
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
serverSock = socket(PF_INET, SOCK_STREAM, 0);
if (serverSock == INVALID_SOCKET)
ErrorHanding("socket() error!");
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(atoi(argv[1]));
if (bind(serverSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("bind() error!");
if (listen(serverSock, 5) == SOCKET_ERROR)
ErrorHanding("listen() error!");
clientAddrSize = sizeof(clientAddr);
clientSock = accept(serverSock, (SOCKADDR *)&clientAddr, &clientAddrSize);
if (clientSock == INVALID_SOCKET)
ErrorHanding("accept() error!");
FILE *fp = fopen(file_name, "rb");
if (fp != NULL)
{
while (1)
{
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE)
{
send(clientSock, (char *)&buf, read_cnt, 0);
break;
}
else
send(clientSock, (char *)&buf, BUF_SIZE, 0);
}
}
shutdown(clientSock, SD_SEND);
recv(clientSock, (char *)buf, BUF_SIZE, 0);
printf("Message from client: %s\n", buf);
fclose(fp);
closesocket(clientSock);
closesocket(serverSock);
WSACleanup();
return 0;
}
file_client_win.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHanding(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET sock;
SOCKADDR_IN serverAddr;
int read_cnt;
char file_name[] = "receive.dat";
char buf[BUF_SIZE];
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHanding("WSAStartup() error!");
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
ErrorHanding("sock() error!");
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
ErrorHanding("connect() error!");
FILE *fp = fopen(file_name, "wb");
while ((read_cnt = recv(sock, buf, BUF_SIZE, 0)) != 0)
fwrite((void *)buf, 1, read_cnt, fp);
printf("Received file data\n");
send(sock, "Thank you", 10, 0);
fclose(fp);
closesocket(sock);
WSACleanup();
return 0;
}
编译:
gcc file_server_win.c -lwsock32 -o fileServWin
gcc file_client_win.c -lwsock32 -o fileClntWin
运行结果:
// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 7>fileServWin 9190
Message from client: Thank you
// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 7>fileClntWin 127.0.0.1 9190
Received file data
接收到的文件 receive.dat 里面的内容就是 file_server_win.c,这里只展示部分内容:
习题
(1)解释 TCP 中 “流” 的概念。UDP 中能否形成流?请说明原因。
TCP的流是指,两台主机通过套接字建立连接后进入可交换数据的状态,也称为“流形成的状态”。也就是把建立套接字后可交换数据的状态看做一种流。
UDP是基于报文面向无连接的,没有建立连接的过程,所以不能形成流。
(2)Linux 中的 close 函数或 Windows 中的closesocket函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情况下会出现问题?
单方面断开连接就是两台主机正在通信,其中一台主机关闭了所有连接,那么一台主机向另一台主机传输的数据可能会没有接收到而损毁。
单方面的断开连接意味着套接字无法再发送数据。一般在对方有剩余数据未发送完成时,断开己方连接,会造成问题。
(3)什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么信息?
半关闭就是把输入流或者输出流关了。
针对输出流执行半关闭的主机处于可以接收数据而不能发送数据。
半关闭会使其发送最后一个报文段时附带一个EOF,告诉对方主机自己没有数据要发了,但还是可以接收对方主机传送的数据。