Linux网络编程 - 优雅地断开TCP套接字连接

2024-05-14 17:08

本文主要是介绍Linux网络编程 - 优雅地断开TCP套接字连接,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一 基于TCP的半关闭

        TCP中的断开连接过程比建立连接过程更重要,因为连接过程中一般不会出现大的变数,但断开过程有可能发生预想不到的情况,因此应准确掌控。只有掌握了下面要讲解的半关闭(Half-close),才能明确断开过程。

1.1 单方面断开连接带来的问题

Linux的close()函数意味着完全断开连接。完全断开连接不仅指无法传送数据,而且也不能接收数据。因此,在某些情况下,通信一方调用close断开连接就显得不太优雅,如下图所示。

图1-1  单方面断开连接

        上图1-1 描述的是2台主机正在进行通信。主机A发送完最后的数据后,调用close()函数断开了连接,之后主机A无法再接收主机B传输的数据。实际上,是完全无法调用与接收数据相关的函数。最终,由主机B传输的、主机A必须接收的数据也销毁了。

        为了解决这类问题,“只关闭一部分数据交换中使用的流”(half-close)的方法应运而生。断开一部分连接是指,可以传输数据但无法接收,或可以接收数据但无法传输。顾名思义就是只关闭流的一半。

1.2 套接字和流(Stream)

        两台主机通过套接字建立连接后进入可交换数据的状态,又称为“流形成状态”。也就是把建立套接字后可交换数据的状态看作一种流。

        此处的流可以比作是水流。水朝着一个分享流动,同样,在套接字的流中,数据也只能向一个方向移动。因此,为了进行双向通信,需要如下图所示的两个流。

图1-2  套接字中生成的两个流

         一旦两台主机间建立了套接字连接,每个主机就会拥有单独的输入流和输出流。当然,其中一个主机的输入流与另一个主机的输出流相连,而输出流则与另一个主机的输入流相连。另外,我们讨论的“优雅地断开连接方式”是指只断开其中一个流,而非同时断开两个流。Linux的close函数将同时断开这两个流。

说明

1、输入缓冲,即接收缓冲;输出缓冲,即发送缓存。

2、I/O流1:主机A输出缓冲 ——> 主机B输入缓冲

3、I/O流2:主机B输出缓冲 ——> 主机A输入缓冲

4、我们说一条流,指的是某一方向上的数据传输逻辑通道。

1.3 针对优雅断开的 shutdown 函数

        在Linux中,使用 shutdown() 函数来关闭其中的一条流。

  • shutdown() — 用于半关闭的函数。
#include <sys/socket.h>int shutdown(int sock, int howto);/*参数说明
sock: 需要断开的套接字文件描述符。
howto: 传递断开方式信息
*///返回值: 成功时返回0,失败时返回-1

        调用shutdown()函数时,第二个参数决定断开连接的方式,其可能值有如下所示:

  • SHUT_RD:断开输入流。
  • SHUT_WR:断开输出流。
  • SHUT_RDWR:同时断开I/O流。

        若向shutdown函数的第二个形参传递实参 SHUT_RD,则断开当前套接字的输入流,即套接字无法接收数据。即使输入缓冲收到数据也要抹去,而且无法调用输入相关函数(read、recv)。如果向shutdown函数的第二个形参传递实参 SHUT_WR,则中断输出流,也就无法传输数据。但如果输出缓冲还留有未传输的数据,则将传递至目标主机。最后,若传入实参 SHUT_RDWR,则同时中断 I/O流。这相当于分两次调用shutdown函数,其中一次传入以 SHUT_RD为实参,另一次以 SHUT_WR为实参。

1.4 为何需要半关闭

        上文我们已对“关闭套接字的一半连接”有了充分的认识,但可能还有一些疑惑。

究竟为什么需要半关闭?是否只要留出足够长的连接时间,保证完成数据交换即可?只要不急于断开连接,好像也没必要使用半关闭。

        这句话也不完全是错的。如果保持足够的时间间隔,完成数据交换后再断开连接,这时就没必要使用半关闭。但要考虑如下情况:

一旦客户端连接到服务器端,服务器端将约定的文件传给客户端,客户端收到后发送字符串 ‘Thank you’ 给服务器端。

        此处字符串 “Thank you” 的传递实际上是多余的,这只是用来模拟客户端断开连接前还有数据需要传递的情况。此时程序实现并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时结束。客户端也没办法无休止地调用输入函数(read、recv),因为这有可能导致程序阻塞(调用的函数未返回)。

是否可以让服务器端和客户端约定一个代表文件尾的字符?

        这种方式也有问题,因为这意味着文件中不能有与约定字符相同的内容。为解决该问题,服务器端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接收 EOF,这样可以避免与文件内容冲突。剩下最后一个问题:服务器如何传递 EOF?

断开输出流时向对方主机传输EOF。

        当然,调用close()函数的同时关闭I/O流,这样也会向对方发送EOF。但此时无法再接收对方传输的数据。换言之,若调用close函数关闭流,就无法接收客户端最后发送的字符串消息“Thank you”。这时需要调用shutdown函数,只关闭服务器的输出流(半关闭)。这样即可以发送EOF,同时又保留了输入流,可以接收对方数据。下面结合前面的内容实现一个收发文件的服务器端/客户端示例程序。

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

        收发文件的服务器端/客户端的数据流可整理如下图所示。

图1-3  文件传输数据流程图
  • 服务器端程序 file_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_sock, clnt_sock;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");if(!fp)error_handling("fopen() error!");serv_sock=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]));if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)error_handling("bind() error!");if(listen(serv_sock, 5) == -1)error_handling("listen() error!");clnt_addr_sz=sizeof(clnt_addr);    clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);while(1)    //使用while循环向客户端传输文件{read_cnt=fread((void*)buf, 1, BUF_SIZE, fp);if(read_cnt<BUF_SIZE){write(clnt_sock, buf, read_cnt);break;}write(clnt_sock, buf, BUF_SIZE);}shutdown(clnt_sock, SHUT_WR);                //关闭套接字的输出流,并发送EOFread(clnt_sock, buf, BUF_SIZE);              //输入流仍可以接收数据printf("Message from client: %s\n", buf);    //控制台打印接收到的字符串消息fclose(fp);close(clnt_sock); close(serv_sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
  • 客户端程序 file_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;char buf[BUF_SIZE];int read_cnt;struct sockaddr_in serv_adr;if(argc!=3) {printf("Usage: %s <IP> <port>\n", argv[0]);exit(1);}fp=fopen("receive.dat", "wb");if(!fp)error_handling("fopen() error!");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]));if(connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)error_handling("connect() error!");while((read_cnt=read(sd, buf, BUF_SIZE )) !=0 )  //当收到EOF时退出循环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);
}
  •  程序运行结果
  • 服务器端

编译程序:gcc file_server.c -o fserver

运行程序:./fserver 9190

Message from client: Thank you

  • 客户端

编译程序:gcc file_client.c -o fclient

运行程序:Received file data

二 习题

1、解释TCP中“流”的概念。UDP中能否形成流?请说明原因

:TCP中的流:是指两台通信主机通过套接字建立TCP连接后进入可交换数据的状态,又称为“流形成的状态”。也就是把建立套接字后可交换数据的状态看作一种流。而对于UDP来说,不存在流,因为UDP是无连接的。

2、Linux中的close函数或Windows中的closesocket函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情况下会出现问题?

:通信一方调用close函数或closesocket函数断开连接时,就是单方面断开连接,这种断开连接方式意味着完全断开连接,不仅无法发送数据,而且也不能接收数据。一般在对方有剩余数据未发送时,己方断开连接,会造成问题。

3、什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么信息?

:TCP是全双工通信方式,通信双方都拥有独立的输入流和输出流,当关闭其中的一个流时,此时该套接字就处于半关闭状态或者说该TCP连接处于半关闭状态。当关闭输出流时,意味着主机无法发送数据了,但是可以继续接收对方发来的数据,此时主机处于 FIN-WAIT(结束等待)状态。

当本端调用shutdown函数,并向第二个参数传入 SHUT_WR,则会断开输出流,进入半关闭状态(关闭输出流,保留输入流),对方主机会收到己方主机发出的 FIN 报文段信息。

参考

《TCP-IP网络编程(尹圣雨)》第7章 - 优雅地断开套接字连接

《TCP/IP网络编程》课后练习答案第一部分6~10章 尹圣雨

这篇关于Linux网络编程 - 优雅地断开TCP套接字连接的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/989337

相关文章

基于Linux的ffmpeg python的关键帧抽取

《基于Linux的ffmpegpython的关键帧抽取》本文主要介绍了基于Linux的ffmpegpython的关键帧抽取,实现以按帧或时间间隔抽取关键帧,文中通过示例代码介绍的非常详细,对大家的学... 目录1.FFmpeg的环境配置1) 创建一个虚拟环境envjavascript2) ffmpeg-py

Linux脚本(shell)的使用方式

《Linux脚本(shell)的使用方式》:本文主要介绍Linux脚本(shell)的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述语法详解数学运算表达式Shell变量变量分类环境变量Shell内部变量自定义变量:定义、赋值自定义变量:引用、修改、删

Linux链表操作方式

《Linux链表操作方式》:本文主要介绍Linux链表操作方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、链表基础概念与内核链表优势二、内核链表结构与宏解析三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势六、典型应用场景七、调试技巧与

详解Linux中常见环境变量的特点与设置

《详解Linux中常见环境变量的特点与设置》环境变量是操作系统和用户设置的一些动态键值对,为运行的程序提供配置信息,理解环境变量对于系统管理、软件开发都很重要,下面小编就为大家详细介绍一下吧... 目录前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变

Linux系统中的firewall-offline-cmd详解(收藏版)

《Linux系统中的firewall-offline-cmd详解(收藏版)》firewall-offline-cmd是firewalld的一个命令行工具,专门设计用于在没有运行firewalld服务的... 目录主要用途基本语法选项1. 状态管理2. 区域管理3. 服务管理4. 端口管理5. ICMP 阻断

Linux实现线程同步的多种方式汇总

《Linux实现线程同步的多种方式汇总》本文详细介绍了Linux下线程同步的多种方法,包括互斥锁、自旋锁、信号量以及它们的使用示例,通过这些同步机制,可以解决线程安全问题,防止资源竞争导致的错误,示例... 目录什么是线程同步?一、互斥锁(单人洗手间规则)适用场景:特点:二、条件变量(咖啡厅取餐系统)工作流

Linux中修改Apache HTTP Server(httpd)默认端口的完整指南

《Linux中修改ApacheHTTPServer(httpd)默认端口的完整指南》ApacheHTTPServer(简称httpd)是Linux系统中最常用的Web服务器之一,本文将详细介绍如何... 目录一、修改 httpd 默认端口的步骤1. 查找 httpd 配置文件路径2. 编辑配置文件3. 保存

Linux使用scp进行远程目录文件复制的详细步骤和示例

《Linux使用scp进行远程目录文件复制的详细步骤和示例》在Linux系统中,scp(安全复制协议)是一个使用SSH(安全外壳协议)进行文件和目录安全传输的命令,它允许在远程主机之间复制文件和目录,... 目录1. 什么是scp?2. 语法3. 示例示例 1: 复制本地目录到远程主机示例 2: 复制远程主

java连接opcua的常见问题及解决方法

《java连接opcua的常见问题及解决方法》本文将使用EclipseMilo作为示例库,演示如何在Java中使用匿名、用户名密码以及证书加密三种方式连接到OPCUA服务器,若需要使用其他SDK,原理... 目录一、前言二、准备工作三、匿名方式连接3.1 匿名方式简介3.2 示例代码四、用户名密码方式连接4

MySQL 表的内外连接案例详解

《MySQL表的内外连接案例详解》本文给大家介绍MySQL表的内外连接,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录表的内外连接(重点)内连接外连接表的内外连接(重点)内连接内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选,我