网络编程之TCP编程

一、TCP基本概述

TCP是一种传输层的网络协议,是一种面向连接的,可靠的,基于字节流的网络协议。进行TCP通信的时候,双方一定要先建立连接,也就是我们所说的三次握手,建立稳定连接之后,接下来就是我们的通信了,接下来就可以进行正常的发送和收发数据。收发数据的时候是基于字节流的。

TCP编程模型

网络编程之TCP编程_第1张图片

 模型解析:

1.首先,服务器端先用户区创建一个socket文件,随后绑定内核空间中一个网卡设备的映射,(为何需要绑定呢,因为我们创建一个socket的时候只是创建了而已,并没有绑定到网卡设备),绑定的时候会进行ip地址和端口号的设置,接下来就监听相应的ip地址和端口号。accept会阻塞等待客户端的连接。

2.客户端会创建一个socket文件并且通过connect去连接服务器。

3.以上两点完成后,客户端和服务器端就创建好连接了,接下来就可以正常收发数据了

4.收发完数据之后,要结束连接,结束连接就要进行四次挥手。

二、socket文件

我们在写TCP代码之前先了解一下socket文件,我们之前学io编程的时候都知道,在Linux当中一切皆文件,文件类型我们分成三种,一种是字符型文件,第二种是块类型文件,第三者网络型设备。对于字符型文件,比如磁盘,我们会在内核空间映射出磁盘的一个file,并且在用户空间通过文件描述符去操控这个file,并且各个file是相互独立的。但是网卡设备略有不同,同样的,我们在用户空间有socket描述符,在内核空间有网卡设备的相应驱动,我们要自己绑定相应的网卡设备地址,唯一不同的是,我们绑定一块网卡地址好像可以同时操控网络上的所有网卡,比如客户端创建了一个socket号,这个socket号不是单单对客户端网卡设备有效,对其他网卡一样有效,所以我们就可以通过相同的socket号同时从客户端和服务器端两个网卡中进行通信。是跨网卡的。说白了客户端和服务器端组成了一个超级网卡,两端可以同时向这个超级网卡写数据,读数据,并且两端同时操作这个超级网卡的时候不会冲突,cpu会采用分时复用进行避免冲突

三、TCP基础通信代码

服务端代码

//服务器端
#include
#include
#include
#include
#include
#include
#include
int main()
{
        int sockfd;//服务端的socket号
        int cnt;
        char redBuff[128];//读取缓冲区
        int connect_fd;//客户端的socket号
        int ret_bind;//绑定函数返回值
        struct sockaddr_in serve_addr;//服务器的网卡设备地址
        socklen_t serve_len,client_len;
        
        //1.创建服务器端socket号
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)                                                         
        {
                perror("sock failed\n");                                                              
                exit(1);
        }
        //2.配置服务器端网卡设备相关参数                                                  
        serve_addr.sin_family = AF_INET;//协议族:IPV4                                                              
        serve_addr.sin_port = 8090;//端口号                                                                   
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//将ip地址由主机字节序转化为网络字节序                                               
        serve_len = sizeof(serve_addr);//服务器端地址长度                                                               
        client_len = sizeof(client_addr);//客户端地址长度                                                             
        //3.socket号绑定网卡设备地址                                                           
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);                              
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }                                                                                             
        //4.监听连接请求
        listen(sockfd,3);                                                                             
        
        //5.等待客户端的连接                                                                
        while(1)
        {
                connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);               
                if(connect_fd < 0)                                                                    
                {
                        perror("connect faile\n");                                                    
                        exit(1);                                                                      
                }
         //6.建立连接,进行通信                                                                        
                            
                while(1)
                {
                        cnt = read(connect_fd,redBuff,128);//通过客户端socket号进行读取数据                     
                        if(cnt < 0)
                        {
                                perror("read fail\n");
                                exit(1);
                        }
                        else if(cnt == 0)
                        {
                                printf("client offline....\n");
                                break;
                        }else
                        {
                        printf("server:%s\n",redBuff);
                        write(connect_fd,"I receive",10);//通过客户端socket号进行发数据
                        }
                }
        }
        return 0;

 客户端代码

//客户端代码
#include
#include
#include
#include
#include
#include
#include
int main()
{
        char redBuff[128];//读取缓冲区
        int write_ret,read_ret;
        int client_fd,server_fd,ret;
        struct sockaddr_in serve_addr;//服务端的网卡设备地址
        //1.创建客户端socket号
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务端网卡设备地址参数
        serve_addr.sin_family = AF_INET;//IPV4协议
        serve_addr.sin_port = 8090;//服务端端口号
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务端的ip地址
        //3.申请连接
        ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
        if(ret == -1)
        {
                perror("connect failed\n");
                exit(1);
        }
        //4.收发数据
        write_ret = write(client_fd,"helloworld\n",16);//通过客户端socket号发送数据给服务端
        if(write_ret < 0)
        {
                perror("write failed\n");
                exit(1);
        }
        printf("client write end\n");
        read_ret = read(client_fd,redBuff,128);//通过客户端socket号接收服务端数据
        if(read_ret < 0)
        {
                perror("read fail\n");
                exit(2);
        }
        printf("client:%s\n",redBuff);
        //5.结束通信
        close(client_fd);
        return 0;
}

四、TCP之三次握手 

三次握手是TCP通信中建立连接环节

1.首先客户端通过connect函数向服务端发送一个请求通信的syn包,并且进入SYN_SEND状态

2.服务端收到这个syn包,并且确认这个syn包,随后服务端通过accept函数再发送一个syn包给客户端表示确认请求

3.客户端再发送一个包给服务端,再次确认

通信图

网络编程之TCP编程_第2张图片

五、关闭连接

我们建立连接,通完信后,我们要将连接关掉,即关闭相应的socket值,调用close函数,如果不关闭socket,内核会一直维护这个socket。我们关闭连接的时候需要进行四次挥手动作。

四次挥手图

网络编程之TCP编程_第3张图片

详解:

1.客户端应用进程调用close函数,TCP会发送一个FIN M包,告知服务器说我要关闭连接了 

2.服务器收到FIN M包后确认一下这个包,执行被动关闭,随后发一个ACK M+1包给客户端表示我已经收到了,随后就会关闭。

3.过一会,服务器主动调用close函数,同时发一个FIN N包告知客户端,说我要关闭了。

4.客户端收到FIN N包后,发一个ACK N+1包告知服务器说我已经收到你的关闭信息了

六、TCP通信之收发数据实例

服务器代码

//服务器端代码
#include
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
#define RSIZE (4*1024)
/*--------------------
该函数具有以下功能:
1.处理客户端发送来的请求信息
2.形参1:cfd,是客户端的socket号
3.形参2: cmd,是客户端发送来的请求信息
-----------------------*/
void cmd_parse(int cfd,char *cmd)
{
        int video_fd;
        int video_readRet;
        char redBuff[RSIZE] = {0};//读取缓冲区
        //1.容错检测,如果是错误信息,则返回
        if( cmd == NULL)
        {
                printf("nothing\n");
                return;

        }
        //2.如果命令是getText,即获取文本
        if(strcmp(cmd,"getText") == 0)
        {
                printf("get text\n");
                write(cfd,"get text",20);
        }
        //3.如果命令是getPhoto,即获取照片
        else if(strcmp(cmd,"getPhoto") == 0)
        {
                printf("getPhoto\n");
                write(cfd,"getPhoto",20);
        }
        //4.如果命令是获取getVideo,即获取视频
        else if(strcmp(cmd,"getVideo") == 0)
        {
                video_fd = open("../mp4/a.mp4",O_RDONLY);//打开一个文件视频
                if(video_fd < 0) return;
                while(1)
                {
                        video_readRet = read(video_fd,redBuff,RSIZE);
                        printf("Ret:%d\n",video_readRet);
                        write(cfd,redBuff,video_readRet);
                        if(video_readRet < RSIZE) break;
                }       
                close(video_fd);
         
        }
        else
        {
                write(cfd,"i do not understand",22);
        }
 
}
int main()
{
        int sockfd;//服务器端socket号
        int cnt;
        struct sockaddr_in client_addr = {0};//客户端的网卡设备地址
        char redBuff[128];//读取缓冲区
        int connect_fd;//客户端socket号
        int ret_bind;//连接函数的返回值,用以检测
        struct sockaddr_in serve_addr;//服务器的网卡设备地址
        socklen_t serve_len,client_len;
        //1.创建服务器端的socket号
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务器端的网卡设备相关参数
        serve_addr.sin_family = AF_INET;//IPV4的协议
        serve_addr.sin_port = 8090;//端口号
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//将协议族的主机字节序转化为网络字节序
        serve_len = sizeof(serve_addr);
        client_len = sizeof(client_addr);
        //3.将socket号绑定服务器端的网卡设备地址
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //4.监听客户端请求
        listen(sockfd,3);

        //5.等待客户端连接
        while(1)
        {
                connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
                if(connect_fd < 0)
                {
                        perror("connect faile\n");
                        exit(1);
                }
                //通信
                while(1)
                {
                        cnt = read(connect_fd,redBuff,128);
                        if(cnt < 0)
                        {
                                perror("read fail\n");
                                exit(1);
                        }
                        else if(cnt == 0)
 {
                                printf("client offline....\n");
                                break;
                        }else
                        {
                                //请求处理函数
                                cmd_parse(connect_fd,redBuff);
                        }
                }
        }

        close(sockfd);
        close(connect_fd);
        return 0;

客户端代码

//客户端
#include
#include
#include
#include
#include
#include 
#include 
#include
#include
#define RSIZE (4*1024)
int main()
{
        char redBuff[RSIZE];//读取缓冲区
        int video_readRet;
        char choose[20] ={0};
        int write_ret,read_ret;
        int client_fd,server_fd,ret;
        struct sockaddr_in client_addr;//客户端网卡设备地址
        int video_fd;
        struct sockaddr_in serve_addr;//服务器端网卡设备地址
        //1.创建客户端socket号
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置客户端的网卡设备地址相关参数
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
        //3.申请连接服务端
        do
        {
                ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
        }while(ret != 0);
        while(1)
        {
                printf("enter the choose[1-16]\n");
                scanf("%s",choose);
                video_fd = open("../mp4/c.mp4",O_WRONLY|O_CREAT,0777);
                write_ret = write(client_fd,choose,20);

                if(video_fd < 0)
                {
                        perror("open video failed\n");
                        return -1;
                }
                while(1)
                {
                        video_readRet = read(client_fd,redBuff,RSIZE);
                        write(video_fd,redBuff,video_readRet);
                        printf("ret:%d\n",video_readRet);
                        if(video_readRet < RSIZE) break;
 }
                close(video_fd);
                sleep(2);
                system("clear\n");
        }
        close(client_fd);
        return 0;

 七、粘包处理案例

1.粘包的引出

我们知道TCP是面向连接,可靠的,基于数据流的一种传输层协议,所以我们在利用TCP进行通信的时候,数据都是以流的形式进行发送,各个数据之间没有明确的边界之分,比如我们有如下开发要求:客户端向服务器端发送请求,要求服务器将其目录下的所有图片发送过来。网卡是存在缓冲区的,即数据会先暂存网卡缓冲区,等待网卡缓冲区满了再发送出去,这样就会有一个情况发生,比如发送a和b两张图片,先发送a,当a的最后一些字节数据被送进缓冲区之后缓冲区还没满,这时由于cpu执行速度很快,所以b的数据就很快被送进网卡缓冲区,这样a和b的数据就被发送出去了,客户端就无法辨别a和b了,这就是粘包问题。

2.粘包的解决法案

解决方案有:延时、定长包、加边界符、非定长的数据包,其中非定长的数据包就是这个包含有这个包的长度和包的名称,先发送包头告知客户端要发生的文件名以及文件的大小,客户端接收到这个包就知道要发送的文件大小,然后按照指定大小进行读取,这样就不会粘包了。

服务端代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (128)//读取缓冲区

struct package
{
        char name[48];//文件名字
        int size;//文件的大小
};
/*----------------------
该函数具有以下功能:
1.处理客户端请求
2.封包,发包
-----------------------*/
void cmd_parse(int cfd,char *cmd)
{
        DIR* dir;
        int video_fd;
        struct dirent* drt;
        char redBuff[RSIZE] = {0};//读取缓冲区
        struct package pag;
        struct stat st;
        int read_ret,write_ret;
        char tempfile[48] = {0};
        if( cmd == NULL)
        {       
                printf("nothing\n");
//              return;

        }
        if(strcmp(cmd,"getText") == 0)
        {
                printf("get text\n");
                write(cfd,"get text",20);
        }
        else if(strcmp(cmd,"getPhoto") == 0)
        {
                printf("getPhoto\n");
                write(cfd,"getPhoto",20);
        }
        else if(strcmp(cmd,"getVideo") == 0)
        {
                //1.打开图片目录
                dir = opendir("../jpg");
                if(dir == NULL) return;
                //2.遍历图片目录
                while((drt = readdir(dir)) != NULL)
                {
                        if((strcmp(drt->d_name,".") == 0 || (strcmp(drt->d_name,"..") == 0))) continue;
                        //2.打包完,并发送包
                        strcpy(pag.name,drt->d_name);//初始化包的名称
sprintf(tempfile,"%s/%s","../jpg",drt->d_name);//字符串拼接,用以打开文件和获取文件大小
                        if(stat(tempfile,&st) != 0)
                        {
                                perror("stat failed\n");
                                exit(1);
                        }
                        pag.size = st.st_size;//初始化包的大小
                        write(cfd,&pag,sizeof(pag));//发送包
                        //3.打开文件并发送文件
                        video_fd = open(tempfile,O_RDONLY);
                        if(video_fd < 0)
                        {
                                perror("open tempfile failed\n");
                                exit(1);
                        }
                        while((read_ret = read(video_fd,redBuff,RSIZE)) > 0)
                        {

                                write_ret = write(cfd,redBuff,read_ret);
                                printf("read_ret:%d  write_ret:%d\n",read_ret,write_ret);
                        }
                        close(video_fd);

                 
                }
                //4.发送一个结束包给客户端
                strcpy(pag.name,"over");
                pag.size = 0;
                write(cfd,&pag,sizeof(pag));

        }
        else
        {
                write(cfd,"i do not understand",22);
        }

}
int main()
{
        int sockfd;
        int cnt;
        struct sockaddr_in client_addr = {0};
        char redBuff[128];
        int connect_fd;
        int ret_bind;
        struct sockaddr_in serve_addr;
        socklen_t serve_len,client_len;
        //1.创建服务器socket号
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.配置服务器网卡地址的相关参数 
        serve_addr.sin_family = AF_INET;//IPV4协议族
        serve_addr.sin_port = 8090;//端口号
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//主机字节序转化为网络字节序
        serve_len = sizeof(serve_addr);//服务端地址长度
        client_len = sizeof(client_addr);//客户端地址长度
        //3.将socket号将网卡地址绑定
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //4.监听客户端连接请求
        listen(sockfd,3);

        //5.等待客户端连接
        while(1)
        {
                连接客户端,并返回客户端的connect_fd号
                connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
                if(connect_fd < 0)
                {
                        perror("connect faile\n");
                        exit(1);
                }
                //收发数据
                while(1)
                {
                        cnt = read(connect_fd,redBuff,128);
                        if(cnt < 0)
                        {
                                perror("read fail\n");
                                exit(1);
                        }
                        else if(cnt == 0)
 {
                                printf("client offline....\n");
                                break;
                        }else
                        {
                                //处理接收到的命令
                                cmd_parse(connect_fd,redBuff);
                        }
                }
        }
        close(sockfd);
        close(connect_fd);
        return 0;
}
                              

客户端代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RSIZE (128)
struct package
{
        char name[48];//文件的名称
        int size;//文件的大小
};
int main()
{
        char redBuff[RSIZE];//读取缓冲区
        int video_readRet;
        char choose[20] ={0};//命令缓冲区
        int write_ret,read_ret;
        int client_fd,server_fd,ret;
        struct sockaddr_in client_addr;
        int video_fd;
        struct sockaddr_in serve_addr;
        struct package pag;
        char tempfile[48] = {0};
        //1.创建客户端socket号
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置网卡设备地址
        serve_addr.sin_family = AF_INET;//IPV4协议
        serve_addr.sin_port = 8090;//端口号与服务器相同
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器端ip地址
        //3.连接
        do
        {
                ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));//连接服务端
        }while(ret != 0);
        while(1)
        {
                //4.输入命令和发送命令
                printf("enter the choose[1-16]\n");
                scanf("%s",choose);
                write_ret = write(client_fd,choose,20);

                while(1)
                {
                        //1.获得数据包
                        read(client_fd,&pag,sizeof(pag));
                        //2.先判断是不是结束包,是的话直接结束
                        if(pag.size == 0 && strcmp(pag.name,"over") == 0) break;
                        //3.文件读写操作
                        sprintf(tempfile,"%s/%s","../cpjpg",pag.name);
                        video_fd = open(tempfile,O_RDWR|O_CREAT|O_TRUNC,0777);
                        if(video_fd < 0)
                        {
                                perror("open failed\n");
                                exit(2);
                        }
                        while( pag.size > 0)
                        {
                                read_ret = read(client_fd,redBuff,RSIZE);
                                write(video_fd,redBuff,read_ret);
                                pag.size = pag.size-read_ret;
                        }


                        close( video_fd);
                }
                sleep(2);
                system("clear\n");
        }
        close(client_fd);
        return 0;
}

8、多进程并发服务 

8.1概述 

并发服务器即同一个时刻可以响应多个客户端的请求,上面建立的服务器和客户端都是一对一的,只能一个客户端连接。常用的并发服务技术有:多进程并发服务器、多线程并发服务器、IO复用并发服务器。这里讲述的是多进程并发服务器

8.2设计思路

就是将建立连接和收发信息分开来做,主进程一直监听是否有客户端进行申请连接,如果有,马上通过fork函数建立一个子进程,该子进程会复制父进程的本身的socket号,并且拿到申请连接客户端的socket号,通过客户端的socket号完成与新客户端完成通信,子进程完成通信后,父进程要完成子进程的资源回收,避免资源的浪费。资源回收利用信号通信,所有的子进程在进程结束后都会触发一个SIGCHLD这个信号,只要在父进程改造这个信号,当收到这个信号后马上回收资源即可。

8.3代码设计

服务器端


#include
#include
#include
#include
#include
#include
#include
#include
#include
//信号改造函数
void sig_handle(int signum)
{
        wait(NULL);
}
int main()
{
        //1.改造信号
        if(signal(SIGCHLD,sig_handle) == SIG_ERR)
        {
                perror("signal failed\n");
                return -1;
        }
        char redBuff[128];//读取缓冲区
        int sockfd;//服务器socket号
        int connect_fd;//客户端socket号
        int cnt;
        pid_t pid;//子进程pid
        struct sockaddr_in serve_addr;//服务器网卡地址
        struct sockaddr_in client_addr;//客户端网卡地址
        int ret_bind;//bind函数的返回值
        socklen_t serve_len,client_len;//服务器和客户端的地址长度
        //2.创建服务器端socket
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //3.设置服务器网卡设备参数
        serve_addr.sin_family = AF_INET;//IPV4协议族
        serve_addr.sin_port = 8090;//端口号
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);//主机字节序转化为网络字节序
        serve_len = sizeof(serve_addr);//地址长度
        client_len = sizeof(client_addr);
        //4.将网卡设备和服务器socket号绑定
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //5.监听端口号和ip
        listen(sockfd,3);
        //6.建立连接和通信
        while(1)
        {
                //这个循环可以保证主线程一直阻塞在这里,除非有新客户端申请连接
                connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
                if(connect_fd < 0)
                {
                        perror("connect faile\n");
                        exit(1);
                }
                //创建子进程
                pid = fork();
                if(pid < 0)
                {
                        perror("fork failed\n");
                        exit(1);
                }
                if(pid == 0)
                {
                        //子进程负责通信
                        close(sockfd);//子线程用不到服务器socket号,只需要客户端socket号,关掉即可。
                        //通信
                        printf("child pid = %u\n",(unsigned int)getpid());
                        while(1)
                        {
                                cnt = read(connect_fd,redBuff,128);
                                if(cnt < 0)
                                {
                                        perror("read fail\n");
                                        exit(1);
                                }
                                else if(cnt == 0)
                                {
                                        printf("client offline....\n");
                                        return 0;
                                }else
                                {
                                        printf("server:%s\n",redBuff);
                                }
                        }
                        close(connect_fd);
                }
                else
                {
                        close(connect_fd);//服务端不需要利用客户端socket号进行通信,故直接关掉即可
                }
        }
        close(sockfd);
        return 0;
}

客户端

#include
#include
#include
#include
#include
#include
#include
int main()
{
        int cnt = 50;
        char redBuff[128];//读取缓冲区
        int write_ret,read_ret;//write函数和read函数的返回值
        int client_fd,ret;//客户端的socket号
        struct sockaddr_in serve_addr;//服务器端网卡设备地址
        //1.创建客户端socket号
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务器端网卡设备地址参数
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器端ip地址
        //3.申请连接
        ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
        if(ret == -1)
        {
                perror("connect failed\n");
                exit(1);
        }
        //通信
        write_ret = write(client_fd,"helloworld\n",16);
        if(write_ret < 0)
        {
                perror("write failed\n");
                exit(1);
        }
        while(cnt--)
        {
                printf("client pid :%u\n",(unsigned int)getpid());
                sleep(1);
        }
        close(client_fd);
        return 0;
}

 网络编程之TCP编程_第4张图片

 9.IO复用并发服务器

9.1概述

我们在多进程并发服务器当中,每个连接的数据收发都要创建一个新的子进程用来通信,这样对资源的消耗十分大。所以引入io设备的复用,接下来我们介绍一下io设备。

9.2IO设备

io设备就是具有输入输出功能的设备,在Linux中,这些设备全被抽象成文件操作符,即通过编写程序操作文件描述符就可以完成io设备的操作,写操作用write函数,读操作用read函数,这些io设备受io控制器的控制,cpu不能直接操作io设备,需要io控制器这个中介,io控制器解析cpu指令,然后去操作io设备。

IO设备通常只有一个,如果存在多个外部主机模块与之进行输入输出通信,则此io设备会出现并发竟态,需要分时复用

9.3select函数 

9.3.1概述

在Linux中,我们可以使用select函数进行io复用,比如,当有多个客户端与服务器连接,那么多个客户端想要与服务器端通信,只能通过服务器端一个网卡,只要网卡工作的够快,那么就可以在发现有数据包过来的情况下及时转移缓冲区数据,实现并发通信。只需要以下三个步骤:

1.及时发现交互数据包

2.查出要与之通信的客户端

3.快速实现收发动作

select函数就可以完成监听工作。

9.3.2select函数

1.头文件

网络编程之TCP编程_第5张图片

2.函数原型 

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)

①参数1-nfds:需要检查的号码最高的文件描述符加1(关键不能错)

②参数2-readfds:这是一个读集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否可读。

③参数3-writefds:这是一个写集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否可写。

④参数4-exceptfds:这是一个错误集合,这个集合里面都是文件描述符,select函数会监听这些描述符,看他们的状态是否有错误。

⑤参数5-timeout:这是一个结构体,表示监听的时间是多久,过了这个时间就会返回0

具体解释select的参数:

(1)intmaxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。

说明:对于这个原理的解释可以看上边fd_set的详细解释,fd_set是以位图的形式来存储这些文件描述符。maxfdp也就是定义了位图中有效的位的个数。

(2)fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

(3)fd_set*writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

(4)fd_set*errorfds同上面两个参数的意图,用来监视文件错误异常文件。

(5)structtimeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

说明:

函数返回:

(1)当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数。

(2)当没有满足条件的文件描述符,且设置的timeval监控时间超时时,select函数会返回一个为0的值。

(3)当select返回负值时,发生错误。

3.函数返回值

①错误时:返回-1

②正确时:返回能读或者能写的文件描述符总数

③超时:返回0

4.结构体struct timeval

struct timeval{      

        long tv_sec;   /*秒 */

        long tv_usec;  /*微秒 */   

    }

有三种情况:

    ①timeout == NULL  等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。

    ②timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

    ③timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。

5.fd_set集合

fd_set是一个二进制数组。如下图:

网络编程之TCP编程_第6张图片

对于 fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型变量的值,或者使用以下几个宏来控制它:

int FD_ZERO(int fd, fd_set *fdset);//将该集合全部清空   

int FD_CLR(int fd, fd_set *fdset);//将fd从fdset集合中清除掉   

int FD_SET(int fd, fd_set *fd_set);//将fd加入到fdset集合中去   

int FD_ISSET(int fd, fd_set *fdset);//判断fd是否被标记

6.select模型理解 

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
7.关于select的几点说明

①使用select函数查询时,一旦fd_set集合中有文件描述符是可读的,那么就会将这个描述符标记,如果文件描述符缓冲区的东西没有被读走,那么下次查询这个文化描述符的时候依然是可读的。我们写个测试案例:

#include
#include
#include
#include
#include

int main()
{
        fd_set rset;
        int select_ret;
        FD_ZERO(&rset);
        while(1)
        {
                FD_SET(0,&rset);//将标准输入文件描述符加入到rset中去
                select_ret = select(1,&rset,NULL,NULL,NULL);
                if(select_ret > 0)
                {
                        if(FD_ISSET(0,&rset))//如果标准输入可读
                        {
                                printf("ok\n");
                                FD_CLR(0,&rset);
                                sleep(1);
                        }
                }
        }
        return 0;
}

 测试结果:

网络编程之TCP编程_第7张图片

 我键盘输入”input“,然后程序就会一直打印ok,即使我使用FD_CLR这个函数将0从rset集合中删除,依然会打印ok。

结果分析:说明如果0那个标记没有被清除,每次select对rset查验的时候总是会返回一个正值。那么如何清除这个标记呢

解决办法:将缓冲区的东西读走,那么标准输入就变成不可读了,即标记清除(其他设备也是一样,将缓冲区内容读走)


#include
#include
#include
#include
#include

int main()
{
        fd_set rset;
        char redBuff[16];//读取缓冲区
        int select_ret;
        FD_ZERO(&rset);
        while(1)
        {
                FD_SET(0,&rset);//将标准输入文件描述符加入到rset中去
                select_ret = select(1,&rset,NULL,NULL,NULL);
                if(select_ret > 0)
                {
                        if(FD_ISSET(0,&rset))//如果标准输入可读
                        {
                                printf("ok\n");
                                scanf("%s",redBuff); 
                                sleep(1);
                        }
                }
        }
        return 0;
}

测试结果:

7438113e861e408d8c919e96e68f6dd2.png

 我输入input,就打印一次ok,因为输入缓冲区的内容被读走了,标准输入0就不是可读的了,所以就不会打印ok

9.4服务器代码

#include
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
int main()
{
        int select_ret,max_fd;//select函数返回值和文件描述集合中最大的文件描述符
        fd_set rset,temp_set;//两个读集合
        int cdf;
        struct timeval time;//select函数的参数4
        int sockfd;//服务器端的socket号
        int cnt;
        struct sockaddr_in client_addr = {0};//客户端网卡设备地址
        char redBuff[128];//读取缓冲区
        int connect_fd;//客户端socket号
        int ret_bind;//bind函数的返回值
        struct sockaddr_in serve_addr;//服务器端的网卡设备地址
        socklen_t serve_len,client_len;//服务器和客户端网卡地址长度
        //1.创建服务器端socket号
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务器端网卡设备地址参数
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
        serve_len = sizeof(serve_addr);
        client_len = sizeof(client_addr);
        //3.将服务器端socket号和服务器网卡设备地址绑定
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //4.监听
        listen(sockfd,3);
        max_fd = sockfd;//最大文件描述符为sockfd
        //5.等待连接
        FD_ZERO(&rset);
        FD_ZERO(&temp_set);
        FD_SET(sockfd,&rset);//将服务器的sockfd加入到rset集合中去
        while(1)
        {
                //初始化timeval
                time.tv_sec = 2;
                time.tv_usec = 1000;

                temp_set = rset;//目的在于将temp_set传给select
                select_ret = select(max_fd+1,&temp_set,NULL,NULL,&time);
                if(select_ret == 0) printf("time out\n");
                else if(select_ret < 0) printf("erro\n");
                else
                {
                //查询到sockfd可读,就是有申请连接
                if(FD_ISSET(sockfd,&temp_set))
                {
                        connect_fd = accept(sockfd,(struct sockaddr*)&client_addr,&client_len);//拿到客户端socket号
                        if(connect_fd < 0)
                        {
                                perror("connect faile\n");
                                exit(1);
                        }
                        FD_SET(connect_fd,&rset);//加入到rset,让select去查询客户端是否有发送数据
                        max_fd = max_fd>connect_fd ? max_fd:connect_fd;//修改最大的fd号
                
                }
                //循环判断连接的客户端是否有信息发过来,即检查客户端socket号是否可读
                else
                {
                        for(cdf = 4; cdf < max_fd+1; cdf++)
                        {
                                //通信
                                if(FD_ISSET(cdf,&rset))
                                {
                                        cnt = read(cdf,redBuff,128);
                                        if(cnt < 0)
                                        {
                                                perror("read fail\n");
                                                exit(1);
                                        }
                                        else if(cnt == 0)
                                        {
                                                //客户端退出
                                                printf("client offline....\n");
                                                FD_CLR(cdf,&rset);//将该文件描述符删除掉,让select不再查询这个文件描述符。
                                                break;
                                        }else
                                        {
                                                printf("cdf = %d  %s\n",cdf,redBuff);
                                                sleep(1);
                                        }
                                }
                        }
                }
                }
        }
        close(sockfd);
        close(connect_fd);
        return 0;
}

 网络编程之TCP编程_第8张图片

 这里不用将sockfd从rset删除,为什么呢,因为第一次我们查询sockfd发现有客户端申请连接,那么就说明网卡缓冲区有内容,然后我们通过accept把缓冲区内容读走之后,sockfd他就变得不可读了,所以就不会重复进来这个分支结构。

9.5客户端代码

#include
#include
#include
#include
#include
#include 
#include 
#include
#include
#define RSIZE (4*1024)
int main()
{
        char redBuff[RSIZE];
        int video_readRet;
        int cnt = 20;
        char choose[20] ={0};
        int write_ret,read_ret;
        int client_fd,server_fd,ret;
        struct sockaddr_in client_addr;
        int video_fd;
        struct sockaddr_in serve_addr;
        //1.create the socket
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.set the serve addr
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
        //3.connect
        do
        {
                ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
        }while(ret != 0);
        while(cnt--)
        {
                printf("cnt = %d\n",cnt);
                write(client_fd,"client",7);
                sleep(1);
        }
        close(client_fd);
        while(1);
        return 0;
}
~   

10.epoll并发服务器

10.1epoll的引出

在第九节中,我们使用select函数来构造io复用并发服务器。但是select有以下缺点:

①高并发场景下耗费资源大:

select的运行过程是,首先你在用户空间要创建一个fd_set集合,该集合是文件描述符集合,也是个二进制数组,随后调用select函数后,内核会拷贝一份一模一样的fd_set集合,并且先在内核里面循环遍历,然后将结果个数再返回到用户空间。拷贝的时候开辟空间消耗大,每次select都要拷贝

②遍历查询的时候是个同步过程:

调用select函数的时候,用户进程是会堵塞的,会等待内核遍历出结果后才会不堵塞,这是个同步过程,所以效率不高。

③select仅仅返回可遍历的个数:

具体哪个文件描述符可以读是需要用户自己遍历的。

基于以上三个缺点,epoll的引出就很有必要。

10.2epoll的优点

网络编程之TCP编程_第9张图片

10.2epoll的工作原理分析 

网络编程之TCP编程_第10张图片

第一步:调用int epoll_create(int size)函数,该函数成功的话会返回一个epoll句柄,size告诉内核此次监听描述符的数目。在内核空间会创建一个eventpoll结构体,里面会有rdlist(检查就绪队列),rb(红黑树),wq(阻塞等待队列)。

第二步:调用int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);并且启动后台服务线程,监听查询红黑树,如果有数据到达网卡,到达epitem,服务线程查询红黑树,若符合,epitem会将fd送到就绪队列,并且唤醒进程,epoll_wait拿走就绪队列的fd的值

网络编程之TCP编程_第11张图片

struct epoll_event {
    uint32_t events;  // epoll 事件类型,包括可读,可写等
    epoll_data_t data; // 用户数据,可以是一个指针或文件描述符等
};

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

个人猜测:

①struct epoll_event结构体就是红黑树单节点,将这个单节点的fd设置为一个新的fd,并且插入到红黑树当中去,epoll_ctl函数有插入红黑树节点的功能,跟我们之前写的插入节点函数差不多

②内核里面有个线程一直查询这个红黑树,有新的fd申请连接了,内核会将那个新的fd跟红黑树上的fd进行比较,如果符合,就传给epoll_wait函数

****以上纯属个人猜测****

第二步操作就是比select函数好的优点:

①每次调用epoll_ctl函数,我们只需要添加一个fd进入红黑树即可,不需要将全部需要监听的fd拷入内核,这大大降低资源消耗和时间消耗。

②一旦创建了红黑树,内核就会启动一个内核服务线程去监听这个红黑树,符合条件的马上放到就绪队列当中去,然后通知进程取走数据。注意是通知,通知完就继续查询。

③红黑树遍历效率高

第三步:调用int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

网络编程之TCP编程_第12张图片

 events这个结构体数组有以下特点:

每次监听完将符合要求的文件描述符放到数组的时候,每次返回都会覆盖一遍数组内容,比如一开始符合的是fd = 1和fd = 2,那么events[0].fd = 1和events[1].fd = 2,下次若fd = 4符合,那么events[0].fd = 4, events[1].fd = 2,第二个元素依然为2,因为没有覆盖完。

10.3代码实例 

#include
#include
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
#define MAX_SIZE 10000
int main()
{
        int i;
        int create_fd;
        int current;
        int epollWait_ret;
        struct epoll_event ev;
        struct epoll_event events[20];//文件描述符接收数组
        int cdf;
        int sockfd;
        int cnt;
        struct sockaddr_in client_addr = {0};
        char redBuff[128];
        int connect_fd;
        int ret_bind;
        struct sockaddr_in serve_addr;
        socklen_t serve_len,client_len;
        //1.创建服务器socket号
        sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务器网卡设备地址参数
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
        serve_len = sizeof(serve_addr);
        client_len = sizeof(client_addr);
        //3.绑定网卡和socket号
        ret_bind = bind(sockfd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //4.监听
        listen(sockfd,3);
        //5.创建epoll
        create_fd = epoll_create(MAX_SIZE);
        if(create_fd < 0)
        {
                perror("epoll_create failed\n");
                exit(1);
        }
        ev.events = EPOLLIN|EPOLLET;//事件设置为可读和边沿触发
        ev.data.fd = sockfd;//监听的描述符
        epoll_ctl(create_fd,EPOLL_CTL_ADD,sockfd,&ev);//插入和监听描述符
        current = 1;//此时监听数目(要实时改变)
        //5.等待客户端连接
        while(1)
        {
                epollWait_ret = epoll_wait(create_fd,events,current,1000);
                if(epollWait_ret < 0)
                {
                        printf("epoll_wait failed\n");
                }
                else if(epollWait_ret == 0)
                {
                        printf("time out\n");
                }
                else
                {
                        //遍历返回的数组
                        for(i = 0; i
#include
#include
#include
#include
#include
#include 
#include 
#include
#include
#define RSIZE (4*1024)
int main()
{
        char redBuff[RSIZE];
        int video_readRet;
        int cnt = 20;
        char choose[20] ={0};
        int write_ret,read_ret;
        int client_fd,server_fd,ret;
        struct sockaddr_in client_addr;
        int video_fd;
        struct sockaddr_in serve_addr;
        //1.create the socket
        client_fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.set the serve addr
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");
        //3.connect
        do
        {
                ret = connect(client_fd,(struct sockaddr*)&serve_addr,sizeof(serve_addr));
        }while(ret != 0);
        while(cnt--)
        {
                printf("cnt = %d\n",cnt);
                write(client_fd,"client",7);
                sleep(1);
        }
        close(client_fd);
        return 0;
}
~

11.UDP通信

UDP报文格式:包含源端口,目的端口号, 用户包长度

网络编程之TCP编程_第13张图片

UDP相对于TCP的优点:

①传输数据快:

前提是为建立连接的TCP,如果建立连接的TCP那一定是TCP通信快。因为UDP不需要建立连接,直接就可以收发数据。

②传输数据有边界:

因为UDP是传输数据包的,所以每个包之间有边界,但是TCP则不同,TCP是以流的形式传输,故TCP会出现粘包的问题。

③可以一对多广播通讯:

UDP的一对多是同时的,TCP的并发服务器的一对多只是分时复用的。

UDP相对于TCP的缺点:

①不可靠,不能保证有序传输:

TCP中含有传输控制(滑动机制和重发机制),重发机制会校验数据包,如果丢包了是需要对方重新发送包的,但是UDP丢包就丢包了,也不管。

UDP和TCP的选择:

①如果需要间歇工作:UDP或者TCP短链接

②如果需要长期工作: TCP

通信模型:

网络编程之TCP编程_第14张图片

网络编程之TCP编程_第15张图片

 网络编程之TCP编程_第16张图片

网络编程之TCP编程_第17张图片

#include
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
#define MAX_SIZE 128
int main()
{
        int cdf;
        int listen_fd;
        int cnt;
        struct sockaddr_in client_addr = {0};
        char redBuff[MAX_SIZE];
        int ret_bind;
        struct sockaddr_in serve_addr;
        socklen_t serve_len,client_len;
        //1.创建服务器端socket
        listen_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
        if(listen_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.设置服务器网卡设备地址参数
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
        serve_len = sizeof(serve_addr);
        client_len = sizeof(client_addr);
        //3.将socket号跟网卡绑定
        ret_bind = bind(listen_fd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");
                exit(2);
        }
        //5.等待客户端连接
        while(1)
        {
                cnt = recvfrom(listen_fd,redBuff,MAX_SIZE,0,(struct sockaddr *)&client_addr,&client_len);
                if(cnt < 0) printf("revfrom data faile\n");
                printf("redBuff:%s\n",redBuff);
                sendto(listen_fd,"get it",7,0,(struct sockaddr *)&client_addr,client_len);
        }
        close(listen_fd);
        return 0;
}

#include
#include
#include
#include
#include
#include 
#include 
#include
#include
#define MAX_SIZE 128
int main()
{
        char redBuff[MAX_SIZE];
        int client_fd;
        struct sockaddr_in client_addr;
        struct sockaddr_in serve_addr;
        socklen_t server_len,client_len;
        //1.create the socket
        client_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
        if(client_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //2.set the serve addr
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;//服务器端口号
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.172");//服务器ip地址
        server_len = sizeof(serve_addr);
        //3.connect
        sendto(client_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
        printf("send finished\n");
        recvfrom(client_fd,redBuff,MAX_SIZE,0,NULL,NULL);
        printf("data:%s\n",redBuff);
        close(client_fd);
        return 0;
}

代码几点说明:

①服务器端bind绑定的是自己的端口号8090,不是客户端的端口号,一般来说bind绑定的都是自己的,而客户端的端口号是随机的

②客户端设置的网卡设备地址参数都是服务器的ip和端口号

12.UDP广播通信

1.UDP广播通信对UDP基础通信有以下改进:

①发送端将发送的数据包包装成广播包发送出去,发送端当成客户端 

②接收端充当服务器,一直接收数据

2.如何封装成广播包:

①将广播包的ip地址设置为广播地址192.168.1.255

②设置好接收方的端口号

发送端: 

#include
#include
#include
#include
#include
#include 
#include 
#include
#include
#define MAX_SIZE 128
int main()
{
        char redBuff[MAX_SIZE];
        int send_fd;
        int on = 1;
        struct sockaddr_in client_addr;
        struct sockaddr_in serve_addr;
        socklen_t server_len,client_len;
        //1.创建发送端socket
        send_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
        if(send_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        //设置成广播模式
        setsockopt(send_fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));

        //2.设置网卡设备地址(广播包设置)
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 8090;//只有端口号为8090的才能接收
        serve_addr.sin_addr.s_addr = inet_addr("192.168.1.255");//ip地址为广播地址
        server_len = sizeof(serve_addr);
        //3.connect
        sendto(send_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
        serve_addr.sin_port = 9000;//再发送一个包给端口号为9000的接收方。
        sendto(client_fd,"hello",6,0,(struct sockaddr*)&serve_addr,server_len);
        recvfrom(client_fd,redBuff,MAX_SIZE,0,NULL,NULL);
        printf("send finished\n");
        printf("data:%s\n",redBuff);
        close(client_fd);
        return 0;
}

接收端:

#include
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
#define MAX_SIZE 128
int main()
{
        int cdf;
        int listen_fd;
        int cnt;
        struct sockaddr_in client_addr = {0};
        char redBuff[MAX_SIZE];
        int ret_bind;
        int on =1; struct sockaddr_in serve_addr;
        socklen_t serve_len,client_len;
        //1.create the socket
        listen_fd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
        if(listen_fd < 0)
        {
                perror("sock failed\n");
                exit(1);
        }
        setsockopt(listen_fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
        //2.set the serve addr
        serve_addr.sin_family = AF_INET;
        serve_addr.sin_port = 9000;
        serve_addr.sin_addr.s_addr = htons(INADDR_ANY);
        serve_len = sizeof(serve_addr);
        client_len = sizeof(client_addr);
        //3.绑定自身的网卡设备地址
        ret_bind = bind(listen_fd,(struct sockaddr*)&serve_addr,serve_len);
        if(ret_bind < 0)
        {
                perror("bind failed\n");

        //5.wait the client to connect
        }
        while(1)
        {
                sendto(listen_fd,"get it",7,0,(struct sockaddr *)&client_addr,client_len);
                printf("rec finished\n");
                cnt = recvfrom(listen_fd,redBuff,MAX_SIZE,0,(struct sockaddr *)&client_addr,&client_len);
                if(cnt < 0) printf("revfrom data faile\n");
                printf("redBuff:%s\n",redBuff);
        }
        close(listen_fd);
        return 0;
}

 

你可能感兴趣的:(网络,tcp/ip,网络协议)