(第七章)优雅地断开套接字连接

七、TCP半关闭

套接字开启过程很简单,基本不会出现太大问题;但是在关闭套接字过程当中会出现各种意想不到的情况发生。因此需要单独说一下TCP套接字的关闭。

7.1 单方面断开连接的问题

在Linux系统当中调用close()关闭套接字意味着完全断开连接,如下图所示

在这里插入图片描述

在主机A给B发完信息之后,随即调用close()进行断开连接,此时主机B收到A的信息之后准备发送数据给A,但此时A的断开连接导致双向通信完全断开,导致A无法收到B发送的信息因此被销毁了,我们不希望出现这种情况,理想的情况应该是,A断开的只是A---->B的单向通路,但没断开B—>A的通路,比较形象一点的解释就是:主机A与主机B之间的通信建立在两条信息流之上,一条从A流向B,另外一条从B流向A;A如果关闭应该关闭的是流向B的信息流,而不应该关闭从B流过来的信息(可能需要接收B的回馈信息)。因此我们需要制定特定规则来解决这个问题。在此之前,我们需要弄清楚流的概念。

7.2 针对优雅断开的shutdown函数

进行半关闭操作的第一个函数

#include<sys/socket.h>
int shutdown(int sock,int howto);
//socket:需要关闭的套接字。
//howto:传递断开方式的信息。这个值决定断开连接方法,一般采用宏进行控制且主要有以下宏定义可以使用:
//      1. SHUT_RD: 断开输入流
//      2. SHUT_WR: 断开输出流
//      3. SHUT_RDWR:同时断开I/O流

7.3 为什么需要半关闭

这个问题通俗一点的解释就是:半关闭是否是解决问题的最好的方法?那我们现在看看有什么其他的可靠方案,注意我们讨论的是如何优雅的断开套接字,而非仅仅关闭了事。

  1. 假如A向B发送完数据之后,等待一段时间然后关闭是否可行?这样也能接收B发过来的信息

    答:可行,但是没必要。有几个问题,比如:等多久?假如此时B发生了阻塞,那么A是否也要一直等下去?亦或是A觉得B发送的信息没有什么价值只是一些问候性回馈,那么此时就没必要等,直接关闭。

  2. 在A发送给B的信息最后传递一个约定俗成的断开连接字符例如:EOF,告诉B我要断开连接了。

    可行,但是A即使告诉了B断开连接,A调用close()函数之后,依然接收不到来自B的信息,因此这种方案不符合我们解决问题的初衷(优雅)。

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

我们写一个服务器\客户端接收文件的程序来实际体验以下这个半关闭。服务端向客户端传递server.c源文件,客户端接收源文件并写入receive.dat文件当中。

//server.c
#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_adr,clnt_adr;
  socklen_t clnt_adr_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_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);        //监听,同时可以处理5个客户端请求
  clnt_adr_sz=sizeof(clnt_adr);
  clnt_sd=accept(serv_sd,(struct sockaddr*)&clnt_adr,&clnt_adr_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);
}
//client.c
#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;
  int read_cnt=0;
  char buf[BUF_SIZE];
  struct sockaddr_in serv_adr;
  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_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("Receive 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);
}

编译运行

gcc server.c -o server
gcc client.c -o client
-----------------------------
./server 9091
./client 127.0.0.1 9091

结果

client:Receive file data.....
server:Message from client:Thank you 

此时工程文件内的Receive.dat文件的内容与server.c一样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值