网络编程 第三天 (UDP编程&IO模型)

    使用UDP协议通信时服务器端与客户端无需提前建立连接,只需要知道对方的套接字地址信息就可以发送数据。服务器端只需创建一个套接字用于接收不同客户发来的请求,经过处理即可通信。

    由于没有事先建立连接,因此使用UDP协议通信时无法保证数据是否成功接收,因此,若需要保证数据可靠性,则不要使用UDP通信。

一、UDP编程——服务器端

使用UDP通信的服务器端的编程流程如下:

如果需要使用UDP协议创建一个服务器,则需要以下步骤:

    -创建套接字

    -绑定套接字

    -接收/发送数据

    -断开连接

相比TCP编程,由于没有监听listen()与接收accept(),因此流程较短,编程相比TCP编程更加简单,但是数据可靠性也不复存在

//与TCP编程相同的函数不再赘述

//注意socket()的参数需要选择SOCK_DGRAM

sendto()函数的用法:

    函数sendto()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)

    函数参数:

        sockfd          需要发送数据的套接字文件描述符

        buf               发送数据的缓冲区首地址

        len               发送数据的缓冲区长度

        flags            通常设定为0

        dest_addr    接收方套接字的IP地址与端口号的结构体

        addrlen        dest_addr结构体的大小

    函数返回值:

        成功:实际发送的字节数

        失败:-1

//send(sockfd,buf,len,0)等价于sendto(sockfd,buf,len,0,NULL,0);

recvfrom()函数的用法:

    函数recvfrom()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

    函数参数:

        sockfd        需要接收数据的套接字文件描述符

        buf            接收数据的缓冲区首地址

        len             接收数据的缓冲区长度

        flags          通常设定为0

        src_addr    发送方套接字的IP地址与端口号的结构体

        addrlen        src_addr结构体的大小

    函数返回值:

        成功:实际接收的字节数

        失败:-1

二、UDP编程——客户端

使用UDP通信的客户端的编程流程如下:

使用UDP协议创建一个访问UDP服务器的客户机,需要以下步骤:

    -创建套接字

    -绑定套接字(可选)

    -接收/发送数据

    -断开连接

//上文出现过的函数不再讲解

示例:使用UDP进行通信,代码分成服务器端和客户端两部分

//服务器端

//文件server.c

#include<stdio.h>

#include<stdlib.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<string.h>

#include<unistd.h>

#include<arpa/inet.h>

#define BUFFER 128

int main(int argc, const char *argv[])

{

    int sockfd;

    struct sockaddr_in servaddr,cliaddr;

    socklen_t peerlen;

    char buf[BUFFER];

    if(argc<3)

    {

        printf("too few arguments\n");

        printf("Usage: %s <ip> <port>\n",argv[0]);

        exit(0);

    }

    if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)

    {

        perror("socket");

        exit(0);

    }

    printf("sockfd = %d\n",sockfd);

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(atoi(argv[2]));

    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if(bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)

    {

        perror("bind");

        exit(0);

    }

    printf("bind success\n");

    peerlen = sizeof(cliaddr);

    while(1)

    {

        if(recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&peerlen)<0)

        {

            perror("recvfrom");

            exit(0);

        }

        printf("Received a message: %s\n",buf);

        strcpy(buf,"Welcome to server");

        sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,peerlen);

    }

    close(sockfd);

    return 0;

}
//客户端

//文件client.c

#include<stdio.h>

#include<stdlib.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<string.h>

#include<unistd.h>

#include<arpa/inet.h>

#define BUFFER 128

int main(int argc, const char *argv[])

{

    int sockfd;

    char buf[BUFFER] = "Hello Server";

    struct sockaddr_in servaddr;

    if(argc<3)

    {

        printf("too few arguments\n");

        printf("Usage: %s <ip> <port>\n",argv[0]);

        exit(0);

    }

    if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)

    {

        perror("socket");

        exit(0);

    }

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(atoi(argv[2]));

    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&servaddr,sizeof(servaddr));

    if(recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL)<0)

    {

        perror("recvfrom");

        exit(0);

    }

    printf("receive from server:%s\n",buf);

    close(sockfd);

    return 0;

}

在使用UDP协议时,可以尝试不运行服务器端直接运行客户端,会发现客户端仍然可以发送信息(只不过不会被接收),而TCP协议必须首先与服务器连接后才可以传输信息。

练习:使用UDP协议连接服务器端与客户端,客户端可以不断向服务器端传输信息,直至输入特定信息(例如"byebye")服务器才会与客户端断开连接

//服务器端

//文件server.c

#include<stdio.h>

#include<stdlib.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<string.h>

#include<unistd.h>

#include<arpa/inet.h>

#define BUFFER 128

int main(int argc, const char *argv[])

{

    int sockfd;

    struct sockaddr_in servaddr,cliaddr;

    socklen_t peerlen;

    char buf[BUFFER];

    if(argc<3)

    {

        printf("too few arguments\n");

        printf("Usage: %s <ip> <port>\n",argv[0]);

        exit(0);

    }

    if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)

    {

        perror("socket");

        exit(0);

    }

    printf("sockfd = %d\n",sockfd);

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(atoi(argv[2]));

    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if(bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)

    {

        perror("bind");

        exit(0);

    }

    printf("bind success\n");

    peerlen = sizeof(cliaddr);

    while(1)

    {

        if(recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&peerlen)<0)

        {

            perror("recvfrom");

            exit(0);

        }

        printf("Received a message: %s",buf);

        if(strncmp(buf,"byebye",6)==0)

        {

            printf("Disconnect\n");

            break;

        }

    }

    close(sockfd);

    return 0;

}

 

//客户端

//文件client.c

#include<stdio.h>

#include<stdlib.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<string.h>

#include<unistd.h>

#include<arpa/inet.h>

#define BUFFER 128

int main(int argc, const char *argv[])

{

    int sockfd;

    char buf[BUFFER];

    struct sockaddr_in servaddr;

    if(argc<3)

    {

        printf("too few arguments\n");

        printf("Usage: %s <ip> <port>\n",argv[0]);

        exit(0);

    }

    if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)

    {

        perror("socket");

        exit(0);

    }

    bzero(&servaddr,sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(atoi(argv[2]));

    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    while(1)

    {

        printf("Please input string(input \"byebye\" to stop):");

        fgets(buf,BUFFER,stdin);

        sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&servaddr,sizeof(servaddr));

        if(strncmp(buf,"byebye",6)==0)

        {

            printf("Disconnect\n");

            break;

        }

    }

    close(sockfd);

    return 0;

}

    运行这个练习代码时,可以尝试不打开服务器端,直接运行数据端发送数据。可以发现客户端是可以直接发送数据的,但是由于服务器端未打开,因此无法接收。而TCP协议的客户端则无法发送数据(Connection refused访问被拒绝)。

    造成这种现象的原因是因为TCP是可靠连接,而UDP是不可靠连接。


一、IO模型

1、IO模型与分类分类

    IO,即input&output,指的是计算机与用户之间进行的数据交互方式。在Linux系统中,不同情况下适用的IO场景也不尽相同。

Linux系统将IO分成5种IO模型,分别是:

    -阻塞型IO(blocking IO)

        最常见的IO模型,大多数的程序的默认IO模型都是阻塞型IO。例如之前我们熟悉的scanf()、read()、write()、recv()、send()、accept()、connect()等函数都使用该IO模型。

    -非阻塞型IO(Non-blocking IO)

        并不常见的IO类型。采用非阻塞型IO的程序会在IO失败时立即返回错误值。

    -IO多路复用(IO Multiplexing)

        当程序中同时处理多路IO时采用。

    -信号驱动型IO(signal driven IO)

        非常罕见,这里不讨论。

    -异步IO(asynchronous IO)

        从Linux内核2.6版本后才引入的IO模型,不太常见。简单说是read()端与write()端可以不同时运行,当读取数据端调用read()时可立即进行其余操作,等待内核返回“数据读取完毕”信号后再将数据读取。

//本文中重点讨论前三种IO模型,即阻塞型IO、非阻塞型IO和IO多路复用。

2、阻塞(blocking)

在IO模型中,我们提到了“阻塞”这个概念。什么是阻塞呢?

    阻塞(blocking)指的是在函数得到调用结果返回之前,由于种种原因进程并未得到操作系统的立即响应,进程暂时被挂起,等待相应事件出现后才会被唤醒。

例如在我们熟悉的scanf()函数中,当我们在代码中使用scanf()读取数据时,若暂时未输入数据,此时进程会一直被挂起直至用户输入相应数据为止。在数据被送入stdin之前,整个进程处在阻塞状态,直至用户输入完成才会继续运行。

    缺点:浪费大量系统资源

3、阻塞型IO

//阻塞型IO模型

阻塞型IO是Linux系统内最普遍的IO模式,大多数程序在默认情况下都使用阻塞型IO进行输入输出。之前学习过的大多数读/写函数都是阻塞型IO,例如:

    -读操作:read()、recv()、recvfrom()等

    -写操作:write()、send()等

    -其他操作:accept()、connect()等

具体还可以细分成 读阻塞 和 写阻塞 两种:

    1.读阻塞(以recvfrom()为例)

    当用户进程调用了一个recvfrom()函数时,此时进程从用户态切换至内核态,kernel开始IO第一阶段:准备数据。在有足够的数据到来之前,内核不会进行进一步动作直至数据全部接收。当所有数据接收完毕,kernel开始IO第二阶段:数据拷贝。kernel将收到的数据全部从内核空间拷贝到用户空间中,然后kernel返回结果。在阻塞型IO的两个阶段(等待数据阶段和拷贝数据阶段),整个用户进程都被阻塞,即从“调用recvfrom()函数”开始,直至“收到内核返回结果”,整段时间内用户进程都处在被阻塞的状态中。

    2.写阻塞

    写阻塞的发生状况比读阻塞少得多。主要发生在当写入的缓冲区空间不足时,后续数据不会立即写入缓冲区,等待缓冲区内重新有写入空间。在等待缓冲区有空间这段时间内进程会出现写阻塞的情况。

4、非阻塞型IO

//非阻塞型IO模型

当我们使用非阻塞型IO进行数据读写时,内核会对本次IO是否能够立即获得数据进行判断:

    -若可以立即得到结果,则直接操作,并返回成功

    -若无法立即得到结果,则会立即返回错误而不会阻塞进程等待

    使用非阻塞型IO时,本次IO是否成功会立即得到结果而不会像阻塞型IO一样一直挂起等待。当然,由于无法得知何时能够得到数据,所以需要一个循环来不停测试数据是否已经可读。该过程称为"polling"(原意为“民意选举”,这里指“调查”)。

    使用非阻塞型IO时,进程需要不断poling内核检查IO操作是否已完成,这是一个十分浪费CPU的操作,因此非阻塞型IO基本不采用。

 

二、多路复用

1、多路复用(IO Multiplexing)概念

    ·设想这个场景:一个机场需要管理许多航班的运行,每个航班有进港、出港、泊停、排位、接客等状态。

    最简单的方法:招聘一大堆空管员,每个空管员专盯一架飞机,全权负责该飞机的每个状态直至交接给下一个机场。若这样做,会出现许多问题:

    -空管中心里聚集了一大批空管员,立即人满为患,若后续还有航班需要管理,新的空管员很难再挤进空管中心(运行空间与处理能力有上限)

    -空管员之间需要协同工作,一大批空管员很快将空管中心变成菜市场(通信成本大幅度增加)

    -空管员需要一些公共资源(例如显示屏、计算机等),当空管员过量,大量的时间将被浪费在抢占这些共有资源上(争夺资源浪费时间)

    那么如何解决呢?现实中我们知道,少数的空管员即可完成一整个机场的航班管理。空管员利用一种叫"flight progress strip"的设备,每当有航班信息更新,就在该设备内记录该航班的状态。

与示例类似,当应用程序需要同时处理多路输入输出流时,该程序需要同时考虑到多方面因素:

    -若使用阻塞型IO,则当前一个IO操作未完成时,后续的IO操作都无法运行

    -若使用非阻塞型IO,则需要不断进行polling轮询IO操作是否完成,十分浪费CPU运行时间

    -若设置多个进程,每个进程独立处理一个输入输出流,则需要进行进程的同步与互斥操作,程序将会更加复杂

比较合理的方案是使用IO多路复用达到目的。

    IO多路复用指的是:当有若干个传输数据的请求时,为了有效利用通信传输线路,将多个传输数据放在同一条通信传输线路传输的方式。

    具体到Linux系统中,当我们有多个文件描述符需要传输数据时,Linux内核会监控每个文件描述符的状态。当有数据到达时,Linux内核激活对应的文件描述符并读取数据。通过这个操作,我们可以实现在一个进程中同时处理多个IO请求的操作。

//有些资料称这种方式为“时分复用”,本质相同

2、多路复用函数

在Linux系统中,常用的实现IO复用的函数有三个:select()、poll()和epoll()函数族

1.select()函数

select()函数是最早(1983年在BSD内)实现IO多路复用的函数。select()的用法如下:

    函数select()

    所需头文件:#include<sys/types.h>

                        #include<sys/time.h>

                        #include<unistd.h>

    函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

    函数参数:

        nfds              所有监控的文件描述符中的最大值加1(注意必须加1否则可能出错)

        readfds         需要监控的所有读的文件描述符

        writefds        需要监控的所有写的文件描述符

        exceptfds      需要监控的所有额外操作的文件描述符(例如错误检查等)

        //readfds、writefds、exceptfds三个参数需要使用特定的函数设定,见下

        //三个参数可以省略,若省略则设置为NULL

        timeout        超时时间。设定select()的等待时长。其中必须设定struct timeval类型结构体变量并使用地址传递,该结构体如下:

            //该结构体在头文件<sys/time.h>中

struct timeval

            {

                long tv_sec;    //秒

                long tv_usec;    //毫秒

            };

        若该参数设定为NULL,则表示一直阻塞直至有文件描述符准备就绪。若不为NULL,则会在指定时长内等待事件发生直至超时返回。

    函数返回值:

        成功:返回处于就绪态并且包含在fd_set结构中的描述符总数

        失败:

            0    超时

            -1    错误

    设定readfds、writefds、exceptfds三个参数需要使用以下几个函数设定:

    void FD_SET(int fd, fd_set *fdset)     //将fd加入fdset

    void FD_CLR(int fd, fd_set *fdset)    //将fd从fdset中剔除

    void FD_ZERO(fdset *fdset)             //清除fdset中所有文件描述符

    int FD_ISSET(int fd, fd_set *fdset)    //判断fd是否在fdset中。在调用select()之后使用

调用select()之后,进程会一直等待直至出现以下某种情况:

    -有文件可读

    -有文件可写

    -超时

示例1:使用select()监控stdin,若stdin在5s内无数据,则打印提示信息;若有数据,则输出数据

#include <stdio.h>

#include <stdlib.h>

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

#define MAXSIZE 128

int main()

{

    fd_set rfds;//监控的文件描述符集合

    struct timeval tv;//设定超时时间

    int retval;

    char readbuffer[MAXSIZE];



    FD_ZERO(&rfds);//使用之前先清空

    FD_SET(0, &rfds);//将stdin(文件描述符为0)加入rfds



    tv.tv_sec = 5;//设定时间5秒

    tv.tv_usec = 0;



    retval = select(1, &rfds, NULL, NULL, &tv);//注意第一个参数为0+1=1

    if (retval == -1)

        perror("select()");

    else if (retval)//监控到某个文件描述符就绪

    {

        printf("Data is available now.\n");

        if(FD_ISSET(0, &rfds)!=0)//需要使用FD_ISSET()查看是哪个文件描述符就绪

        {

            fgets(readbuffer,MAXSIZE,stdin);

            printf("string:%s",readbuffer);

        }

    }

    else//超时

        printf("No data within five seconds.\n");



    return 0;

}

示例2:在TCP服务器端使用select(),监控stdin与客户端连接。若stdin中有数据则将stdin内数据输出,若有客户端连接则向客户端发送信息

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/select.h>

#include <sys/socket.h>

#include <arpa/inet.h>

#define N 64



int main(int argc, const char *argv[])

{

    int i, listenfd, connfd, maxfd;

    char buf[N];

    fd_set rdfs;

    struct sockaddr_in myaddr;

    if(argc<3)

    {

        printf("too few argument\n");

        printf("Usage: %s <ip> <port>\n",argv[1]);

        exit(0);

    }

    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)

    {

        perror("fail to socket");

        exit(-1);

    }



    bzero(&myaddr, sizeof(myaddr));

    myaddr.sin_family = AF_INET;

    myaddr.sin_port = htons(atoi(argv[2]));

    myaddr.sin_addr.s_addr = inet_addr(argv[1]);



    if (bind(listenfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0)

    {

        perror("fail to bind");

        exit(-1);

    }

    listen(listenfd, 5);

    maxfd = listenfd;//由于stdin的文件描述符为0,所以maxfd是listenfd



    while(1)

    {

        FD_ZERO(&rdfs);//使用之前先清空rdfs

        FD_SET(0, &rdfs);//将stdin加入rdfs

        FD_SET(listenfd, &rdfs);//将listenfd加入rdfs

        //select()会修改传入的值,因此每次都需要重新设置这三个函数

        if (select(maxfd+1, &rdfs, NULL, NULL, NULL) < 0)//超时时间设定为NULL

        {

            perror("fail to select");

            exit(-1);

        }



        for(i=0; i<=maxfd; i++)

        {

            if (FD_ISSET(i, &rdfs))//监控到数据,查看数据来源

            {

                if (i == STDIN_FILENO)//来自stdin

                {

                    printf("STDIN is coming\n");

                    fgets(buf, N, stdin);

                    printf("string:%s", buf);

                }

                else if (i == listenfd)//来自客户端

                {

                    connfd = accept(i, NULL, NULL);

                    printf("New Connection connfd=%d is coming\n", connfd);

                    strcpy(buf,"Hello, client");

                    send(connfd,buf,N,0);

                    bzero(buf, N);

                    close(connfd);

                }

            }

        }

    }

    return 0;

}   

2.poll()函数与epoll()函数

select()是最早实现IO多路复用的函数,但是它暴露出了许多问题:

    -select()有可能修改传入参数,这样对于一个需要多次调用的函数是非常不友好的(每次都需要重新设定)

    -select()仅仅会返回,但不会告知哪个文件描述符得到了数据,因此需要FD_ISSET()筛选。如果文件描述符过多则十分浪费时间

    -select()最多只能监控1024个文件描述符(且无法更改)

    -select()不是线程安全的。如果将一个fd加入select()且突然想要关闭该文件描述符,则程序会崩溃

1997年,poll()函数被发明。poll()修复了select()的一些问题,例如:

    -poll()去除了监控上限1024个

    -poll()不再修改传入的数据

 

poll()的用法:

    函数poll()

    所需头文件:#include<poll.h>

    函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    函数参数:

        fds        需要参与测试的文件描述符数组。该参数是一个struct pollfd类型的结构体数组,该结构体如下:

struct pollfd

            {

                int fd;            //需要测试的文件描述符

                short events;    //需要测试的事件

                short revents;    //实际发生的事件(即返回结果)

            }

            events与revents的取值如下:

                POLLIN        是否有数据可读

                POLLOUT        是否有数据可写

                POLLERR        是否发生错误(仅能检测输出用文件描述符)

                POLLHUP        是否被挂起(Hang up,仅能检测输出用文件描述符)

                POLLNVALfd    是否是一个打开的文件(仅能检测输出用文件描述符)

        nfds    fds数组内结构体的数量

        timeout    超时时间,单位为毫秒

    函数返回值:

        成功:>0    数组fds中检测成功的文件描述符的总数量

        失败:

            0      超时

            -1    错误

 

但是poll()仍然不是线程安全的。于是2002年,David Libenzi发明了poll()的取代方案epoll()

epoll()可以视为多路IO复用的最终实现,它修复了select()和poll()的绝大多数问题,例如:

    -epoll()是线程安全的,因此无需考虑线程互斥

    -在没有数据准备就绪时,epoll()不会轮询(即不占用CPU),而select()则占用CPU

    -epoll()不仅返回有数据准备就绪,还直接返回对应的文件描述符,因此无需额外的选择代码

//注意:epoll()并不是一个函数而是一个函数族,包括epoll_create()、epoll_create1()、epoll_ctl()、epoll_wait()等函数

//epoll()本质是添加了信号驱动型IO的多路复用,在内核中使用mmap共享用户空间与内核空间,并使用回调函数。在这里不再展开描述

//epoll()唯一的缺点是只有Linux可用......

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值