学习笔记
单方面断开连接带来的问题
Linux的close()
函数意味着完全断开连接,完全断开后,套接字既无法传输数据,也无法接收数据。
Linux 的 close 函数和 Windows 的 closesocket 函数都意味着完全断开连接。也就是无法发送也无法接收数据,有时候这不太优雅。
建立 TCP 套接字连接后可交换数据的状态可以看成一种流(包括输入流和输出流)。close 将会同时断开两个流。
还有一种方法是断开一部分连接:只断开输入流或输出流。一方在发送完所有数据后可以只关闭输出流但保留输入流,这样还可以接收对方的数据。参考[[客户端进程怎么判断服务端进程是否已经写完所有数据#socket 流文件]]
套接字和流
一旦两台主机建立了套接字连接,每个主机就会拥有单独的输入流与输出流。一个主机的输入流与另一台主机的输出流相连,输出流与另一台主机的输入流相连。
半关闭函数shutdown()
#include <sys/socket.h>
int shutdown(int sock, int howto);
// 功能:半关闭套接字
// 参数:sock:需要断开的套接字;howto:断开方式
// 返回值:成功时返回 0,失败时返回 -1。
第二个参数 howto 将决定关闭的方式,可取的值如下:
- SHUT_RD:断开输入流,此后套接字无法接收数据;
- SHUT_WR:断开输出流,此后套接字无法发送数据;
- 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
- 解释 TCP 中 “流” 的概念。UDP 中能否形成流?请说明原因
TCP 的流指,两台主机通过套接字建立连接后进入可交换数据的状态,也称为 “流形成的状态”。也就是把建立套接字后可交换数据的状态看做一种流。UDP 没有建立连接的过程,所以不能形成流。
Q02
- Linux 中的
close
函数或 Windows 中的closesocket
函数属于单方面断开连接的方法,有可能带来一些问题。什么是单方面断开连接?什么情况下会出现问题?
单方面的断开连接意味着套接字无法再发送数据。一般在对方有剩余数据为发送完成时,断开己方连接,会造成问题。
单方面断开连接就是两台主机正在通信,其中一台主机关闭了所有连接,那么一台主机向另一台主机传输的数据可能会没有接收到而损毁。传输文件的服务器只需连续传输文件数据即可,而客户端不知道需要接收数据到何时。客户端也没有办法无休止的调用输入函数。现在需要一个 EOF 代表数据已经传输完毕,那么这时就需要半关闭,服务端把自己的输出流关了,这时客户端就知数据已经传输完毕,因为服务端的输入流还没关,客户端可以给服务器汇报,接收完毕。
Q03
- 什么是半关闭?针对输出流执行半关闭的主机处于何种状态?半关闭会导致对方主机接收什么信息?
半关闭就是把输入流或者输出流关了。针对输出流执行半关闭的主机处于可以接收数据而不能发送数据。半关闭会导致对方主机接收一个 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
*******************************************/