《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 30void 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 30void 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;}elsesend(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 30void 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,告诉对方主机自己没有数据要发了,但还是可以接收对方主机传送的数据。