《TCP/IP网络编程》学习笔记 | Chapter 7:优雅地断开套接字连接

《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为参数。

为何需要半关闭?

  1. 数据传输完成: 当一方已经发送完所有需要发送的数据,但仍然需要接收对方的响应或数据时,可以使用半关闭。这样,发送方可以告诉对方已经没有更多的数据要发送了。
  2. 错误处理: 如果一方在通信过程中遇到错误,它可能会选择关闭发送方向,以防止发送更多的数据,同时仍然监听对方可能发送的错误响应或状态信息。
  3. 保持连接: 在某些应用场景中,即使数据传输已经完成,一方可能仍希望保持连接,以便在将来需要时重新使用,而不是重新建立连接。
  4. 优雅地关闭连接: 在TCP连接中,半关闭允许一方在发送完所有数据后优雅地关闭连接,而不是突然断开,这有助于另一方正确地处理连接的关闭。
  5. 调试和诊断: 在调试网络应用程序时,半关闭可以帮助开发者理解数据流和连接状态,从而更容易地诊断问题。

比如服务器给客户端发数据,发完后客户端回一个 “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,告诉对方主机自己没有数据要发了,但还是可以接收对方主机传送的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值