一、网络编程概要(三)

 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;
}

服务器结果如图:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值