学习笔记

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

Linux的close()函数意味着完全断开连接,完全断开后,套接字既无法传输数据,也无法接收数据。

Linux 的 close 函数和 Windows 的 closesocket 函数都意味着完全断开连接。也就是无法发送也无法接收数据,有时候这不太优雅。

建立 TCP 套接字连接后可交换数据的状态可以看成一种流(包括输入流和输出流)。close 将会同时断开两个流。

还有一种方法是断开一部分连接:只断开输入流或输出流。一方在发送完所有数据后可以只关闭输出流但保留输入流,这样还可以接收对方的数据。参考[[客户端进程怎么判断服务端进程是否已经写完所有数据#socket 流文件]]

客户端套接字退出establish状态_输出流

套接字和流

一旦两台主机建立了套接字连接,每个主机就会拥有单独的输入流与输出流。一个主机的输入流与另一台主机的输出流相连,输出流与另一台主机的输入流相连。

客户端套接字退出establish状态_客户端套接字退出establish状态_02

半关闭函数shutdown()

#include <sys/socket.h>
int shutdown(int sock, int howto);

// 功能:半关闭套接字
// 参数:sock:需要断开的套接字;howto:断开方式
// 返回值:成功时返回 0,失败时返回 -1。

第二个参数 howto 将决定关闭的方式,可取的值如下:

  1. SHUT_RD:断开输入流,此后套接字无法接收数据;
  2. SHUT_WR:断开输出流,此后套接字无法发送数据;
  3. SHUT_RDWR:同时断开 I/O 流。

他们的值按序分别是 0, 1, 2;

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

为何要半关闭

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

当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。

习题答案

Q01

  1. 解释 TCP 中 “流” 的概念。UDP 中能否形成流?请说明原因
    TCP 的流指,两台主机通过套接字建立连接后进入可交换数据的状态,也称为 “流形成的状态”。也就是把建立套接字后可交换数据的状态看做一种流。UDP 没有建立连接的过程,所以不能形成流。

Q02

  1. Linux 中的 close 函数或 Windows 中的closesocket函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情况下会出现问题?
    单方面的断开连接意味着套接字无法再发送数据。一般在对方有剩余数据为发送完成时,断开己方连接,会造成问题。
    单方面断开连接就是两台主机正在通信,其中一台主机关闭了所有连接,那么一台主机向另一台主机传输的数据可能会没有接收到而损毁。传输文件的服务器只需连续传输文件数据即可,而客户端不知道需要接收数据到何时。客户端也没有办法无休止的调用输入函数。现在需要一个 EOF 代表数据已经传输完毕,那么这时就需要半关闭,服务端把自己的输出流关了,这时客户端就知数据已经传输完毕,因为服务端的输入流还没关,客户端可以给服务器汇报,接收完毕。

Q03

  1. 什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么信息?
    半关闭就是把输入流或者输出流关了。针对输出流执行半关闭的主机处于可以接收数据而不能发送数据。半关闭会导致对方主机接收一个 EOF 文件结束符。对方就知道你的数据已经传输完毕。

书本源码

01-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_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("01-file_server.c", "rb"); //传输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)
	{
		//size_t fread( void *buffer, size_t size, size_t count,FILE *stream );

		/*
		buffer	-	pointer to the array where the read objects are stored
		size	-	size of each object in bytes
		count	-	the number of the objects to be read
		stream	-	the stream to read
		*/
		read_cnt=fread((void*)buf, 1, BUF_SIZE, fp);//将文件写入buf数组中
		if(read_cnt<BUF_SIZE)//说明写到文件尾部了
		{
			write(clnt_sd, buf, read_cnt);
			break;
		}
		write(clnt_sd, buf, BUF_SIZE);//传给客户端
	}
	
	shutdown(clnt_sd, SHUT_WR);	//发送文件后针对输出流进行半关闭。这样就向客户端传输了EOF,而客户端也知道文件传输已完成。
	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);
}

/******************** input******************
*description:
功能:向客户端传输服务器端源文件01-file_server.c,最后还可以确认服务器端已正常接收客户端最后传输的消息“Thank you”。

*content:

./01-file_server 9190

*******************************************/



/******************** output******************
*description:




*content:

Message from client: Thank you 


*******************************************/

02-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");
	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));
	

	//如果服务端进程关闭了socket连接,那么客户端会接收到服务端发送过来的一个 TCP 协议的 FIN 数据包(EOF)
	//然后客户端进程中原本阻塞着等待接收服务端进程数据的 read函数此时就会被唤醒,返回一个值 0。
	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);
}

/******************** input******************
*description:

接收来自客户端发送的源文件,并存储到receive.dat中

*content:

./02-file_client 127.0.0.1 9190

*******************************************/



/******************** output******************
*description:




*content:

Received file data

*******************************************/