1.多种I/O函数
本来想把这部分写在一部分里面,但一部分太多,因此写在这里。
1.send函数和recv函数
(1)send函数
ssize send(int sockfd,const void *buf,size_t nbytes,int flags);
//成功时返回发送的字节数,失败时返回-1
1.sockfd
表示与数据传输对象的连接的套接字文件描述符。
2.buf
保存待传输数据的缓冲地址值
3.nbytes
待传输的字节数
4.flags
传输数据时可指定的可选项信息
(2)recv函数
ssize_t recv(int sockfd,void *buf,size_t nbytes,int flags);
//成功时返回接收的字节数(收到EOF时返回0),失败时返回-1
(1)sockfd
表示数据接收对象的连接的套接字文件描述符。
(2)buf
保存接收数据的缓冲地址值。
(3)nbytes
可接收的最大字节数。
(4)flags
接收数据时指定的可选项信息。
1.send函数和recv函数的最后一个参数flags
send函数和recv函数的最后一个参数是收发数据时的可选项。
该选项可以使用位或运算符同时传递多个信息。下面是可选项:
2.深入flags可选项
1.MSG_OOB
先介绍带外数据(Out Of Band,OOB),带外数据使用与普通数据不同的通道独立传送给用户,是相连的每一对流套接口间一个逻辑上独立的传输通道。
TCP和UDP中都没有真正的带外数据,但是TCP利用其头部中的紧急指针标志和紧急指针字段来传输紧急数据,给应用程序提供了一种传输紧急数据的方式,这种传输紧急数据的方式与带外数据类似,所以TCP紧急数据也就可以被称为带外数据。
MSG_OOB可选项用于创建特殊发送方法和通道以发送紧急消息。但带外缓存只有一个字节,因此MSG_OOB真正的意义在于督促数据接收对象尽快处理数据。
代码示例:
服务端:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<fcntl.h>
#define BUF_SIZE 50
char p[BUF_SIZE];
int serverSock;
int clientSock;
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
void urgHandler(int sig)
{
char rcv[BUF_SIZE];
int readLen=recv(clientSock,rcv,BUF_SIZE-1,MSG_OOB);
rcv[readLen]=0;
printf("Urgent message: %s\n",rcv);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
struct sigaction act;
act.sa_handler=urgHandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(serverSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
if(bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("bind() error!");
if(listen(serverSock,5)==-1)
printMess("listen() error!");
struct sockaddr_in clientAddr;
socklen_t clientAddrLen=sizeof(clientAddr);
clientSock=accept(serverSock,(struct sockaddr*)&clientAddr,&clientAddrLen);
fcntl(clientSock,F_SETOWN,getpid());
int state=sigaction(SIGURG,&act,0);
int readLen=0;
while(readLen=recv(clientSock,p,BUF_SIZE-1,0))
{
if(readLen==-1)
continue;
p[readLen]=0;
puts(p);
}
close(serverSock);
close(clientSock);
return 0;
}
客户端:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define BUF_SIZE 50
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int clientSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(clientSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
if(connect(clientSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("connect() error!");
write(clientSock,"123",strlen("123"));
send(clientSock,"4",strlen("4"),MSG_OOB);
write(clientSock,"567",strlen("567"));
send(clientSock,"890",strlen("890"),MSG_OOB);
close(clientSock);
return 0;
}
不知道为什么这个结果不和书上的相同,但我又重新将书上的代码打了一遍,发现结果也是这个。疑问放在这里。
先来讨论代码中的函数:fcntl(recv_sock,F_SETOWN,getpid());
getpid函数的返回值是当前进程pid。
上面fcntl函数含义是文件描述符clientSock指向的套接字引发的SIGURG信号处理进程变为以getpid函数返回值为ID的进程。
多个进程可以拥有1个套接字的文件描述符,因此假如通过调用fork函数创建子进程,并同时复制文件描述符,且此时发生SIGURG信号,应当调用哪个进程的信号处理函数呢?
因此,处理SIGURG信号时,必须指定处理信号的进程。
个人觉得这里的fcntl有点多余,因为没有fork函数创建子进程,可能是作者让我们多学点东西吧。
紧急模式工作原理:
上面的URG表示本报文段中发送的数据是否包含紧急数据,为1则包含,反之不包含。
URG指针为3,说明数据段位序为3的字节之前是紧急数据,位序从0开始。
但如上面客户端的结果所示,除紧急指针的前面1个字节外,数据接收方将通过调用常用输人函数读取剩余部分。换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。
2.MSG_PEEK和MSG_DONTWAIT
同时设置MSG_PEEK和MSG_DONTWAIT选项,以验证输入缓冲中是否存在接收的数据。
设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。
因此,设置MSG_DONTWAIT搭配,以无阻塞的方式验证待读取数据存在与否。
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int clientSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(clientSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
if(connect(clientSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("connect() error!");
write(clientSock,"123",strlen("123"));
close(clientSock);
return 0;
}
服务端:
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<string.h>
#define BUF_SIZE 50
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=2)
printMess("argc error!");
int serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(serverSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("bind() error!");
if(listen(serverSock,5)==-1)
printMess("connect() error!");
struct sockaddr_in clientAddr;
socklen_t clientAddrLen=sizeof(clientAddr);
int readLen1=recv(clientSock,p,BUF_SIZE-1,MSG_PEEK|MSG_DONTWAIT);
p[readLen1]=0;
if(readLen1>0)
printf("Read : %s\n",p);
//当然也可以用recv函数
char q[BUF_SIZE];
int readLen2=read(clientSock,q,BUF_SIZE-1);
q[readLen2]=0;
printf("Read Again: %s\n",q);
close(clientSock);
close(serverSock);
return 0;
}
结果:
2.readv函数和writev函数
readv和writev是对数据进行整合传输即接收的函数。
即通过writev函数可以将分散保存在多个缓冲的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此,适当使用这两个函数可以减少I/O函数的使用次数。
1.writev函数
ssize_t writev(int filedes,const struct iovec *iov,int iovcnt);
//成功时返回发送的字节数,失败时返回-1,并设置相应的errno。
(1)filedes
传递传输数据的文件(或套接字)文件描述符。
(2)iov
iovec结构体数据的地址值,结构体iovec中包含待发送数据的位置和大小信息。
vec表示是个向量,也就是个数组。
(3)iovcnt
向第二个参数传递的数组长度
iovec结构体如下:
具体如下:
示例代码:
#include<stdio.h>
#include<sys/uio.h>
int main(int argc,char *argv[])
{
char p1[]="abcdefg";
char p2[]="1234567";
struct iovec vec[2];
vec[0].iov_base=p1;
vec[0].iov_len=3;
vec[1].iov_base=p2;
vec[1].iov_len=4;
int readLen=writev(1,vec,2);
puts("");
printf("read bytes : %d\n",readLen);
return 0;
}
2.readv函数
ssize_t readv(int filedes,const struct iovec *iov,int iovcnt);
//成功返回接收的字节数,失败时返回-1
(1)filedes
传递接收数据的文件(或套接字)文件描述符。
(2)iov
包含数据保存位置和大小信息的iovec结构体数组的地址值。
(3)iovcnt
第二个参数值数组的长度
示例代码:
#include<stdio.h>
#include<sys/uio.h>
#define BUF_SIZE 30
int main()
{
struct iovec vec[2];
char buf1[BUF_SIZE]={0,};
char buf2[BUF_SIZE]={0,};
vec[0].iov_base=buf1;
vec[0].iov_len=5;
vec[1].iov_base=buf2;
vec[1].iov_len=BUF_SIZE;
int readLen=readv(0,vec,2);
printf("Read1 : %s\n",buf1);
printf("Read2 : %s\n",buf2);
return 0;
}
~
结果:
2.多播与广播
假如向1000名用户发送同样的数据,如果基于TCP提供服务,则需要维护1000个套接字、
即使使用UDP套接字提供服务,也需要1000次数据传输,对服务器端和网络流量产生负面影响。
1.多播
多播(Multicast)方式的数据传输是基于UDP完成的。因此,与UDP服务器端和客户端的实现方式非常接近。
区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入注册的特定组的大量主机。
因此,采用多播方式时,可以同时向多个主机传递数据。
多播的数据传输特点如下:
1.多播服务器端针对特定多播组,只发送1次数据。
2.即使只发生一次数据,但该组的所有客户端都会接收数据
3.多播组数可在IP地址范围内任意增加
4.加入特定组即可接收发往该多播组的数据
多播组是D类IP地址(224.0.0.0~239.255.255.255),多播是基于UDP完成的。因此,多播数据包的格式与UDP数据包相同。
但与一般的UDP数据包不同,像网络传递1个多播数据包时,路由器将复制该数据包并传递到多个主机。
为了设置多播数据包,防止网络阻塞必须设置TTL。TTL是Time To Live的简写,是决定数据包“传送距离”的主要因素。TTL用整数表示,并且每经过1个路由器就减1。当TTL为0时,该数据包无法再被传递,只能销毁。
因此,TTL设置过大影响网络流量。设置过小,可能会无法传递到目标。
设置TTL需要修改套接字的可选项,如下:
我在网上找了一圈,没有提到创建多播组的概念,似乎只有加入多播组的概念。
关于加入多播组,先来介绍ip_mreq结构体。
第一个成员imr_multiaddr中写入加入的组IP地址。
第二个成员是加入该组的套接字所属主机的IP地址,也可使用INADDR_ANY。
加入多播组需要设置套接字的可选项,如下:
多播中用发送者(即Sender)和接收者(即Receiver)替代服务器端和客户端。
示例的各部分功能:
Sender:向AAA组广播(Broadcasting)文件中保存的新闻信息。
Receiver:接收传递到AAA组的新闻信息。
发送者:
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define BUF_SIZE 30
#define TTL 64
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int sendSock=socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(sendSock==-1)
printMess("socket() error!");
struct sockaddr_in multiAddr;
memset(&multiAddr,0,sizeof(multiAddr));
multiAddr.sin_family=AF_INET;
multiAddr.sin_port=htons(atoi(argv[1]));
multiAddr.sin_addr.s_addr=inet_addr(argv[2]);
int timeLive=TTL;
setsockopt(sendSock,IPPROTO_IP,IP_MULTICAST_TTL,(void*)&timeLive,sizeof(timeLive));
FILE* fp=fopen("news.txt","r");
if(!fp)
printMess("fopen() error!");
while(!feof(fp))
{
fgets(p,BUF_SIZE,fp);
sendto(sendSock,p,strlen(p),0,(struct sockaddr*)&multiAddr,sizeof(multiAddr));
sleep(2);
}
fclose(fp);
close(sendSock);
return 0;
}
接收者:
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<string.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int recvSock=socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(recvSock==-1)
printMess("socket() error!");
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(atoi(argv[1]));
addr.sin_addr.s_addr=inet_addr("127.0.0.1");
if(bind(recvSock,(struct sockaddr*)&addr,sizeof(addr))==-1)
printMess("bind() error!");
struct ip_mreq joinAddr;
joinAddr.imr_multiaddr.s_addr=inet_addr(argv[2]);
joinAddr.imr_interface.s_addr=inet_addr("127.0.0.1");
setsockopt(recvSock,IPPROTO_IP,IP_ADD_MEMBERSHIP,(void*)&joinAddr,sizeof(joinAddr));
int i=0;
while(1)
{
int readLen=recvfrom(recvSock,p,BUF_SIZE-1,0,NULL,0);
if(readLen<0)
break;
p[readLen]=0;
printf("time %d completed: %s\n",++i,p);
}
close(recvSock);
return 0;
}
这里我和书对照了几百遍,但就是收不到组播的数据包,等以后学完Linux系统再来弄。
2.广播
和多播类似,广播也能一次性向多个主机发送数据。
但和多播不同的是,广播传输数据的范围有所不同。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。
多播也是基于UDP完成的,根据传输数据时,使用的IP地址的形式,广播分为以下2种:
1.直接广播(Directed Broadcast)
2.本地广播(Local Broadcast)
两者在代码上的差别在于IP地址。
直接广播的IP地址除了网络地址外,其余主机地址全部设置为1。
例如,向网络地址192.12.34的所有主机传输数据时,可以向192.12.34.255传输。即可以采用直接广播的方式向特定区域内的所有主机传输数据。
本地广播中使用的IP地址限定为255.255.255.255。
例如,192.12.34网络中的主机向255.255.255.255传输数据时,数据将传递到192.12.34网络的所有主机上。
3.套接字和标准I/O
1.标准IO的两大优点
(1)标准IO具有良好的移植性(portability)
(2)标准IO可以利用缓冲提高性能
但要注意的是,在创建套接字时,操作系统将生成用于IO的缓冲。此缓冲在执行TCP协议时,发挥着非常重要的作用。此时若使用标准I/O函数,将会得到另一缓冲的支持。如图:
套接字缓冲的主要目的是为了实现TCP协议,而I/O缓冲的目的是为了提高性能。
2.标准I/O的几个缺点
1.不容易进行双向通信
2.有时可能频繁使用fflush函数
3.需要以FILE结构体指针的形式返回文件描述符
3.FILE结构体指针和文件描述符的双向转换
(1)文件描述符转FILE结构体指针
1.fdopen函数
FILE* fdopen(int filedes,const char *mode);
//成功时返回转换的FILE结构体,失败时返回NULL
1.filedes
需要转换的文件描述符
2.mode
将要创建的FILE结构体指针的模式信息
示例代码:
#include<stdio.h>
#include<fcntl.h>
#include<stdlib.h>
int main(void)
{
int fd=open("fopen.dat",O_WRONLY|O_CREAT|O_TRUNC);
if(fd==-1)
{
fputs("file open error",stderr);
exit(1);
}
FILE *fp=fdopen(fd,"w");
fputs("Network C programing\n",fp);
fclose(fp);
return 0;
}
打开fdopen.dat有下面的内容:
(2)FILE结构体指针转文件描述符
2.fileno函数
int fileno(FILE *stream);
//成功时返回转换后的文件描述符,失败时返回-1
代码示例:
#include<stdio.h>
#include<fcntl.h>
#include<stdlib.h>
int main(void)
{
int fd1=open("fileno.dat",O_WRONLY|O_CREAT|O_TRUNC);
FILE *fp=fdopen(fd1,"w");
int fd2=fileno(fp);
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
}
服务端:
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(serverSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
if(bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("bind() error!");
if(listen(serverSock,5)==-1)
printMess("listen() error!");
struct sockaddr_in clientAddr;
socklen_t clientAddrLen=sizeof(clientAddr);
int clientSock=accept(serverSock,(struct sockaddr*)&clientAddr,&clientAddrLen);
printf("client connected......\n");
if(clientSock==-1)
printMess("accept() error!");
FILE *fp1=fdopen(clientSock,"r");
FILE *fp2=fdopen(clientSock,"w");
while(!feof(fp1))
{
fgets(p,BUF_SIZE,fp1);
fputs(p,fp2);
fflush(fp2);
}
fclose(fp1);
fclose(fp2);
close(serverSock);
close(clientSock);
}
客户端:
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
void printMess(char *mess)
{
fputs(mess,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
if(argc!=3)
printMess("argc error!");
int clientSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if(clientSock==-1)
printMess("socket() error!");
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
if(connect(clientSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
printMess("connect() error!");
FILE *fp1=fdopen(clientSock,"r");
FILE *fp2=fdopen(clientSock,"w");
while(1)
{
fputs("input string,q or Q to quit!\n",stdout);
fgets(p,BUF_SIZE,stdin);
if(!strcmp(p,"q\n")||!strcmp(p,"Q\n"))
break;
fputs(p,fp2);
fflush(fp2);
fgets(p,BUF_SIZE,fp1);
printf("server message: %s\n",p);
}
fclose(fp1);
fclose(fp2);
close(clientSock);
}
4.关于半关闭和I/O分流
关于I/O分流,之前学习过两种:
1.已学IO分离种类
(1)TCP I/O过程分离
这种方法通过调用fork函数复制出1个文件描述符,以区分输入输出中使用的文件描述符。
虽然文件描述符本身不会根据输入和输出进行区分,但分开了2个文件描述符的用途。
(2)fdopen函数实现分离
创建读模式FILE指针和写模式FILE指针,分离了输入工具和输出工具。
个人想法:
还有一种是通过两个管道将I/O分离的方法,但和上面服务器端和客户端之间发送接收信息不同的是,该方法用于父子进程之间信息的交换。
半关闭函数shutdown可以解决第一种I/O分离。而第二种方法fclose关闭写FILE指针,会直接终止套接字,而不是半关闭。如下:
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
int main(int argc,char *argv[])
{
int serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
listen(serverSock,5);
struct sockaddr_in clientAddr;
socklen_t clientAddrLen=sizeof(clientAddr);
int clientSock=accept(serverSock,(struct sockaddr*)&clientAddr,&clientAddrLen);
FILE* readFp=fdopen(clientSock,"r");
FILE* writeFp=fdopen(clientSock,"w");
fputs("123\n",writeFp);
fputs("456\n",writeFp);
fputs("789\n",writeFp);
fflush(writeFp);
fclose(writeFp);
fgets(p,BUF_SIZE,readFp);
fputs(p,stdout);
fclose(readFp);
return 0;
}
服务器端结果:
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/socket.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
int main(int argc,char *argv[])
{
int clientSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
connect(clientSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
FILE* readFp=fdopen(clientSock,"r");
FILE* writeFp=fdopen(clientSock,"w");
while(1)
{
if(fgets(p,BUF_SIZE,readFp)==NULL)
break;
fputs(p,stdout);
fflush(writeFp);
}
fputs("OK!",writeFp);
fclose(readFp);
fclose(writeFp);
}
客户端结果:
然而发现,我们只是在服务器端先fclose了写模式的FILE指针,但之后的服务端却接收不到客户端发送的信息了。
2.终止流无法半关闭的原因
读模式FILE指针和写模式FILE指针都是基于同一文件描述符创建的。
因此,针对任意一个FILE指针调用fclose函数都会关闭文件描述符,也就是终止套接字。
销毁套接字时再也无法进行数据交换。那如何进入可以输出但无法输出的半关闭状态呢?
创建FILE指针前先复制文件描述符即可。因为只有销毁掉所有文件描述符才能销毁套接字。
但注意,这和之前调用fork函数复制文件描述符的方式不同,但文件描述符的值不能够重复,因此需要函数来帮助复制。
(1)dup和dup2函数
int dup(int fildes);
//成功返回复制的文件描述符,失败返回-1
1.fildes
需要复制的文件描述符
int dup2(int fildes,int fildes2);
//成功时返回复制的文件描述符,失败时返回-1
1.fildes
需要复制的文件描述符
2.fildes2
明确指定的文件描述符整数值
代码示例:
#include<stdio.h>
#include<unistd.h>
int main(int argc,char *argv[])
{
char p1[]="Hello\n";
char p2[]="It is a nice day~\n";
int fd1=dup(1);
int fd2=dup2(fd1,10);
printf("fd1=%d,fd2=%d\n",fd1,fd2);
write(fd1,p1,sizeof(p1));
write(fd2,p2,sizeof(p2));
close(fd1);
close(fd2);
write(1,p1,sizeof(p1));
close(1);
write(1,p1,sizeof(p1));
return 0;
}
结果:
复制的结果如下:
此时针对写模式FILE指针调用close函数时,只能销毁与该FILE指针相关的文件描述符,无法销毁套接字。如下,调用fclose函数后还剩一个套接字,但此时并没有进入半关闭状态,要进入真正的半关闭状态需要特殊处理。
在销毁一个文件描述符之后,我们在用shutdown另一个文件描述符,进入半关闭状态。
或者先shutdown一个文件描述符,再销毁该文件描述符,也能进入半关闭状态,因为对文件描述符的操作都可以视为对套接字的操作。
新的服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 30
char p[BUF_SIZE];
int main(int argc,char *argv[])
{
int serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in serverAddr;
memset(&serverAddr,0,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr=inet_addr(argv[2]);
bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
listen(serverSock,5);
struct sockaddr_in clientAddr;
socklen_t clientAddrLen=sizeof(clientAddr);
int clientSock=accept(serverSock,(struct sockaddr*)&clientAddr,&clientAddrLen);
FILE* readFp=fdopen(clientSock,"r");
//不同处1
FILE* writeFp=fdopen(dup(clientSock),"w");
fputs("123\n",writeFp);
fputs("456\n",writeFp);
fputs("789\n",writeFp);
fflush(writeFp);
//不同处2
shutdown(fileno(writeFp),SHUT_WR);
fclose(writeFp);
fgets(p,BUF_SIZE,readFp);
fputs(p,stdout);
fclose(readFp);
return 0;
}
服务器结果如图: