第7章 优雅的断开套接字连接

7.1基于TCP的半关闭

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

Linux的close函数和Windows的closesocket函数意味着完全断开连接。完全断开不仅指无法传输数据,而且也不能接受数据。 主机发送完最后数据后,调用close函数断开连接,之后主机A无法在接收主机B传输的数据。完全无法调用接受数据相关函数。所以主机B传输的数据会被销毁。为了解决这一问题,“只关闭一部分数据交换中使用的流”

2.套接字和流

两台主机套接字建立连接进入可交换数据状态又称为“流形成状态”。 一旦两台主机间建立了套接字连接,每个主机就会拥有单独的输入输出流。
一个主机的输入流与另一主机的输出流相连,而输出流则与另一主机的输入流相连。

3.针对优雅断开的shutdown函数

#include<sys/socket.h>
int shutdown(int sock,int howto)
成功时返回0,失败时返回-1
sock        需要断开的套接字的文件描述符
howto       传递断开方式信息

链接断开方式三种方法

  • SHUT_RD: 断开输入流
  • SHUT_WR: 断开输出流
  • SHUT_RDWR: 同时断开输入输出流

4.为何要半关闭

传输文件的服务端只需连续传输文件数据,而客户端不知道需要接受数据到什么时候,如果客户端一直调用输入函数可能会导致程序阻塞。
所以服务端和客户端要约定一个字符代表传输结束————EOF,服务端向客户端传送完成后会发送EOF告诉客户端,而这个EOF是在关闭输出流时发送。
可以调用close函数同时关闭I/O流,也会发送EOF,但是此时没法再接收来自另一方的也就是客户端还想传递的数据。
所以要用shutdown函数半关闭。只关闭输出流。这样能发送EOF还能保留输入流接受对方数据。

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


服务端

#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"); //打开文件以向客户端file_server.c文件
	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)
	{
		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_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("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);
}

7.2基于Windows的实现

#include<winsock2.h>
int shutdown(SOCKET sock,int howto)
成功时返回0,失败时返回SOCKET_ERROR
sock        要断开的套接字句柄
howto       断开方式的信息

对应三种断开方式

  • SD_RECEIVE: 断开输入流
  • SD_SEND: 断开输出流
  • SD_BOTH: 同时断开输入输出流

常量名不同但是对应的数字相同,断开输入流:0,断开输出流:1,全断开:2

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	FILE * fp;
	char buf[BUF_SIZE];
	int readCnt;
	
	SOCKADDR_IN servAdr, clntAdr;
	int clntAdrSz;

	if(argc!=2) {
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 
	
	fp=fopen("file_server_win.c", "rb"); 
	hServSock=socket(PF_INET, SOCK_STREAM, 0);   
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));
	
	bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
	listen(hServSock, 5);
	
	clntAdrSz=sizeof(clntAdr);    
	hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSz);
	
	while(1)
	{
		readCnt=fread((void*)buf, 1, BUF_SIZE, fp);
		if(readCnt<BUF_SIZE)
		{
			send(hClntSock, (char*)&buf, readCnt, 0);
			break;
		}
		send(hClntSock, (char*)&buf, BUF_SIZE, 0);
	}
	
	shutdown(hClntSock, SD_SEND);	
	recv(hClntSock, (char*)buf, BUF_SIZE, 0);
	printf("Message from client: %s \n", buf);
	
	fclose(fp);
	closesocket(hClntSock); closesocket(hServSock);
	WSACleanup();
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	FILE *fp;
	
	char buf[BUF_SIZE];
	int readCnt;
	SOCKADDR_IN servAdr;

	if(argc!=3) {
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 

	fp=fopen("receive.dat", "wb");
	hSocket=socket(PF_INET, SOCK_STREAM, 0);   

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=inet_addr(argv[1]);
	servAdr.sin_port=htons(atoi(argv[2]));

	connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr));
	
	while((readCnt=recv(hSocket, buf, BUF_SIZE, 0))!=0)
		fwrite((void*)buf, 1, readCnt, fp);
	
	puts("Received file data");
	send(hSocket, "Thank you", 10, 0);
	fclose(fp);
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

本文章使用limfx的vscode插件快速发布