TCP/IP网络编程笔记Chapter I -5基于半关闭的文件传输程序 & shutdown函数

本文介绍了TCP套接字中的IO缓冲,阐述了基于TCP的半关闭连接,包括单方面断开连接的问题、shutdown()函数的使用及其必要性。通过一个基于半关闭的文件传输程序示例,展示了如何在服务器端发送文件后关闭输出流,并接收客户端的反馈数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


如前所述,TCP套接字的数据收发无边界,在上一节 实现一个简单TCP服务器端与客户端的最后,在客户端我们调用了一次write()函数写入,但多次调用了read()函数。这让我们产生了疑问,假设每次传输40字节,而一次接受10字节,那么剩下的30字节在哪等候呢?TCP套接字中的IO缓冲将告诉我们答案。

1.TCP套接字中的IO缓冲

事实上,write()函数调用后并非立即传输数据,read()函数调用后也并非马上接收数据,更准确的说write()函数调用瞬间,数据移至输出缓冲并在适当的时候(不论是一次还是多次传送)传向对方的输入缓冲;read()函数调用瞬间,从输入缓冲中读取数据。
在这里插入图片描述
I/O缓冲特性如下:

  • I/O缓冲在创建套接字时自动生成,在每个TCP套接字中单独存在
  • 关闭套接字会继续传递输出缓冲中的数据
  • 关闭套接字将丢失输入缓冲的数据

2.基于TCP的半关闭

(1)单方面断开连接带来的问题

Linux和Windows的closesocket函数意味着完全断开连接,在某些情况下可能带来问题。若A、B相互通信,A发送完最后的数据后调用close()函数断开连接,之后A无法再发送数据给B,但是由主机B发送的,A必须接收的数据也销毁了。
在这里插入图片描述

(2)shutdown()函数

两台主机通过套接字建立连接后可进入交换数据的状态,我们可以把它视作是一种流(像水流一样朝一个方向流动)。
在这里插入图片描述
我们使用shutdown函数可以用来关闭其中一个流。

	#include<sys/socket.h>
	int shutdown(int sock,int howto);
  • 成功时返回0,失败时返回-1
  • sock是需要断开套接字的文件描述符
  • howto传递断开方式信息
howto的参数含义
SHUT_RD断开输入流(会抹去输入缓冲的数据)
SHUT_WR断开输出流(会继续传递输出缓冲中的数据)
SHUT_RDWR同时断开I/O流

(3)为何必须半关闭

有的朋友可能会有疑惑,究竟为什么需要半关闭,如果留出足够长的时间来保证完成数据传输不就行了吗?没错,保留足够长的时间确实不需要半关闭,但是需要考虑如下情况:假设服务器只用来传输文件数据,而客户端无法知道数据接收到何时,客户端也不能无休止的调用输入函数。为解决此问题,服务器端应传递EOF告诉客户端表示文件传输结束这样就可以了。那么服务器怎么传递呢?shutdown函数应运而生,它可以关闭服务器端的输出流并传递EOF,又保留了输入流,用来接收客户端的消息。

3.基于半关闭的文件传输程序

下面我们结合以学内容实现一个收发文件的服务器端与客户端。整体流程如下,使用服务器端发送一个.c文件给客户端,发送结束服务器端后半关闭输出流,最后验证服务器端输入流不会受到影响,接受客户端发送的thank you。
在这里插入图片描述

(1)服务器端

服务器端创建并发送file_server.c给客户端,传输完成后半关闭输出流再读取客户端回传的数据。

#define BUF_SIZE 30
char buf[BUF_SIZE];
FILE *fp;
fp = fopen("file_server.c", "rb");
//每次读取30个字节,不够30字节则全部读取
while (1)
    {
        read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
        if (read_cnt < BUF_SIZE)
        {
            write(clnt_sd, buf, BUF_SIZE);
            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);

其中fread()函数函数原型如下
在这里插入图片描述
省略掉错误处理的完整代码如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
    int serv_sd, clnt_sd;
    FILE *fp;
    char buf[BUF_SIZE];
    int read_cnt;
 
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;
 
    if (argc != 2)
    {
        printf("Usage:%s<port>\n", argv[0]);
        exit(1);
    }
 	//传递file_server.c的内容
    fp = fopen("file_server.c", "rb");
    serv_sd = socket(PF_INET, SOCK_STREAM, 0);
 
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
 
    bind(serv_sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
    listen(serv_sd, 5);
 
    clnt_adr_sz = sizeof(clnt_adr);
    clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
    while (1)
    {//每次读取30个字节,不够30字节则全部读取
        read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
        if (read_cnt < BUF_SIZE)
        {
            write(clnt_sd, buf, BUF_SIZE);
            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;
}

(2)客户端

客户端创建并把收到数据写入receive.dat中,最后回传Thank you

#define BUF_SIZE 30 
FILE *fp;
fp = fopen("receive.dat", "wb");
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);

完整代码如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 30 
int main(int argc, char *argv[])
{
    int sd;
    FILE *fp;
 
    char buf[BUF_SIZE];
    int read_cnt;
    struct sockaddr_in serv_adr;
    if (argc != 3)
    {
        printf("Usage:%s<port>\n", argv[0]);
        exit(1);
    }
    fp = fopen("receive.dat", "wb");
    sd = socket(PF_INET, SOCK_STREAM, 0);
 
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));
 
    connect(sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
 
    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;
}

(3)运行

服务器端
在这里插入图片描述
客户端
在这里插入图片描述
可以看出新创建的receive.dat文件
在这里插入图片描述
打开后可以看到传入的file_server.c代码
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值