第十五章 套接字

来源:互联网 发布:淘宝的网络经营模式 编辑:程序博客网 时间:2024/05/16 12:13

15.1 什么是套接字

套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行

套接字的创建和使用与管道是有区别的,因为套接字明确的将客户和服务器区分开来.套接字机制可以实现将多个客户连接到一个服务器

15.2 套接字连接

一个简单的本地客户:client1.c

#include <sys/types.h>#include <sys/socket.h>#include <sys/un.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>int main(){    int sockfd;    int len;    struct sockaddr_un address;    int result;    char ch = 'A';    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);    address.sun_family  = AF_UNIX;    strcpy(address.sun_path, "server_socket");    len = sizeof(address);    result = connect(sockfd, (struct sockaddr *)&address, len);    if(result == -1){        perror("oops:client1");        exit(1);    }    write(sockfd, &ch, 1);    read(sockfd, &ch, 1);    printf("char from server = %c\n", ch);    close(sockfd);    exit(0);}

一个简单的本地服务器:server.c

#include <sys/types.h>#include <sys/socket.h>#include <sys/un.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>int main(){    int server_sockfd, client_sockfd;    int server_len, client_len;    struct sockaddr_un server_address;    struct sockaddr_un client_address;    unlink("server_socket");    server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);    server_address.sun_family = AF_UNIX;    strcpy(server_address.sun_path, "server_socket");    server_len = sizeof(server_address);    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);    listen(server_sockfd, 5);    while(1){        char ch;        printf("server waiting\n");        client_len = sizeof(client_address);        client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len);        read(client_sockfd, &ch, 1);        ch++;        write(client_sockfd, &ch, 1);        close(client_sockfd);    }}

15.2.1 套接字属性

套接字的特性由三个属性确定,他们是:域(domain),类型(type)和协议(protocol).

1. 套接字的域

域指定套接字通信中使用的网络介质.最常见的套接字域是AF_INET,它指的是internet网络,许多Linux局域网使用的都是该网络,因特网自身用的也是它.其底层的协议—-网际协议(IP)只有一个地址族,他使用一种特定的方式来指定网络中的计算机,即人们常说的IP地址.

服务器在特定的端口等待客户的连接.知名服务所分配的端口号在所有的Linux和UNIX机器上都是一样的.它们通常小于1024,比如打印机缓冲队列进程(515), rlogin(513), ftp(21)和httpd(80)等.其中最后一个就是web服务器的标准端口.一般情况下,小于1024的端口号都是为系统服务保留的,并且所服务的进程必须具有超级用户权限.

2. 套接字类型

一个套接字域可能有多种不同的通信方式,而每种通信方式又有不同的特性.但AF_UNIX域的套接字没有这样的问题,他们提供了一个可靠的双向通信路径.

因特网协议提供了两种通信机制:流(stream)数据报(datagram).他们有着截然不同的服务层次

流套接字

流套接字(在某些方面类似于标准的输入/输出流)提供一个有序,可靠,双向字节流的连接.因此,发送的数据可以确保不会丢失,复制或乱序到达,并且在这一过程中发生的错误也不会显示出来.大的消息将被分片,传输,再重组.这很像一个文件流,它接受大量的数据,然后以小数据块的形式将他们写入底层磁盘.流套接字的行为是可以预见的

流套接字有类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现的.他们也是AF_UNIX域中常用的套接字类型

TCP/IP代表的是传输控制协议/网际协议,IP协议是针对数据包的底层协议,它提供从一台计算机通过网络到达另一台计算机的路由.TCP协议提供排序,流控和重传,以确保大数据的传输可以完整的到达目的地或报告一个适当的错误条件

数据报套接字

与流套接字相反,由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接.它可以对发送的数据报的长度有限制.数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序到达.

数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供的是一种无需的不可靠服务(UDP代表的是用户数据报协议).但从资源的角度来看,相对来说他们开销比较小,因为不需要维持网络连接.而且因为无需花费时间来建立连接,所以他们的速度也很快

数据报适用于信息服务中的”单次(single-shot)”查询,它主要用来提供日常状态信息或执行低优先级的日志记录.它的优点是服务器的崩溃不会给客户造成不便,也不会要求客户重启,因为基于数据报的服务器通常不保留连接信息,所以可以在不打扰客户的前提下停止并重启

3. 套接字协议

只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议.

15.2.2 创建套接字

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字

#include <sys/types.h>#include <sys/socket.h>int socket(int domain, int type, int protocol);
  1. 创建的套接字是一条通信线路的一个端点.domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议
  2. domain参数可以指定的协议族如下:
     
    AF_UNIX : UNIX域协议(文件系统套接字)
    AF_INET : ARPA因特网协议(UNIX网络套接字)
    AF_ISO : ISO标准协议
    AF_NS : 施乐(Xerox)网络系统协议
    AF_IPX : Novell IPX协议
    AF_APPLETALK : Appletalk DDS
  3. 最常用的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX和Linux文件系统实现本地套接字,后者用于UNIX网络套接字.AF_INET套接字可以用于通过包括因特网在内的TCP/IP网络进行通信的程序.
  4. 参数type指定用于新套接字的通信特性.它的取值包括SOCK_STREAM和SOCK_DGRAM
  5. SOCK_STREAM是一个有序,可靠,面向连接的双向字节流.对AF_INET域套接字来说,它默认是通过一个TCP连接来提供这一特性的,TCP连接在两个流套接字端点之间建立.数据可以通过套接字进行双向传递.TCP协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据
  6. SOCK_DGRAM是数据报服务.我们可以用它来发送最大长度固定(通常比较小)的消息.但消息是否会被正确传递或消息是否不会乱序到达并没有保证.对于AF_INET域套接字来说,这种类型的通信是由UDP数据报来提供的
  7. 通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择.只有当需要选择时,我们才会用到protocol参数.将该参数设置为0表示使用默认协议.
  8. socket系统调用返回一个描述符,它在许多方面都类似于底层的文件描述符.当这个套接字连接到另外一段的套接字之后,我们就可以用read和write系统调用,通过这个描述符来在套接字上发送和接收数据.close系统调用用于结束套接字连接

15.2.3 套接字地址

每个套接字域都有自己的地址格式.对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件sys/un.h中

struct sockaddr_un {    sa_family_t sun_family;     /* AF_UNIX */    char        sun_path[];     /* pathname */};
  1. 对套接字进行处理的系统调用可能需要接受不同类型的地址,每种地址格式都使用一种类似的结构来描述,他们都以一个指定的地址类型(套接字域)的成员(sun_family)开始.在AF_UNIX域中,套接字地址由结构中的sun_path成员中的文件名所指定.
  2. sa_family_t是短整数类型.sun_path指定的路径名长度也是有限制的

在AF_INET域中,套接字地址由结构sockaddr_in来指定,该结构定义在头文件netinet/in.h中,它至少包含以下几个成员

struct sockaddr_in {    short int           sin_family; /* AF_INET */    unsigned short int  sin_port;   /* Port number */    struct in_addr      sin_addr;   /* Internet address */};

IP地址结构in_addr被定义为:

struct in_addr {    unsigned long int   s_addr;};

IP地址中的4个字节组成一个32位的值.一个AF_INET套接字由他的域,IP地址和端口号来完全确定.从应用程序的角度来看,所有套接字的行为就像文件描述符一样,并且通过一个唯一的整数值来区分

15.2.4 命名套接字

要想让通过socket调用创建的套接字可以被其它进程使用,服务器程序就必须给该套接字命名.这样,AF_UNIX套接字就会关联到一个文件系统的路径名,如在server1例子中所看到的.AF_INET套接字则会关联到一个IP端口号

#include <sys/socket.h>int bind(int socket, const struct sockaddr *address, size_t address_len);
  1. bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字.地址结构的长度由参数address_len传递
  2. 地址的长度和格式取决于地址族.bind调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *)
  3. bind调用在成功时返回0,失败时返回-1并设置errno为下列值之一:
     
    EBADF : 文件描述符无效
    ENOTSOCK : 文件描述符对应的不是一个套接字
    EINVAL : 文件描述符对应的是一个已命名的套接字
    ASDDRNOTAVAIL : 地址不可用
    EADDRINUSE : 地址已经绑定了一个套接字

15.2.5 创建套接字队列

为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求

#include <sys/socket.h>int listen(int socket, int backlog);
  1. Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制.为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值.
  2. 在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字.再往后的连接将被拒绝,导致客户端的连接请求失败
  3. listen函数提供的这种机制允许当服务器正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理.backlog参数常用的值是5
  4. listen函数在成功时返回0,失败时返回-1.错误代码包括EBADF,EUNVAL和ENOTSOCK,其含义与上面bind系统调用中说明的一样

15.2.6 接受连接

一旦服务器程序创建并命名了套接字之后,她就可以通过accept系统调用来等待客户建立对该套接字的连接

#include <sys/socket.h>int accept(int socket, struct sockaddr *address, size_t *address);
  1. accept系统调用只有当有客户程序试图连接到由socket参数指定的套接字上时才返回.这里的客户是指,在套接字队列中第一个未处理连接.
  2. accept函数将创建一个新套接字来与客户端进行通信,并且返回新套接字的描述符.新套接字的类型和服务器监听套接字类型是一样的
  3. 套接字必须事先由bind调用命名,并且有listen调用给它分配一个连接队列.连接客户的地址将被放入到address参数指向的sockaddr结构中.如果我们不关心客户的地址,也可以将address参数指定为空指针
  4. 参数address_len指定客户结构的长度.如果客户地址的长度超过这个值,它将被截断
  5. 如果套接字队列中没有未处理的连接,accept将阻塞(程序将暂停)直到有客户建立连接为止.我们可以通过对套接字文件设置O_NONBLOCK标志来改变这一行为,使用的函数fcntl

    int flags = fcntl(socket, F_GETFL, 0);fcntl(socket, F_SETFL, O_NONBLOCK|flags);
  6. 当有未处理的客户连接时,accept函数将返回一个新的套接字文件描述符.发生错误时,accept函数将返回-1.

15.2.7 请求连接

客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器.他们通过connect调用来完成这一工作

#include <sys/socket.h>int connect(int socket, const struct sockaddr *address, size_t address_len);
  1. 参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定.参数socket指定的套接字必须是通过socket调用获得的一个有效的文件描述符
  2. 成功时,connect调用返回0,失败时返回-1.可能的错误代码如下:
     
    EBADF : 传递给socket参数的文件描述符无效
    EALREADY : 该套接字上已经有一个正在进行中的连接
    ETIMEOUT : 连接超时
    ECONNREFUSED : 连接请求被服务器拒绝
  3. 如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间.一旦这个超时时间到达,连接将被放弃,connect调用失败.但如果connect调用被一个信号中断,而该信号又得到了处理,connect调用还是会失败(errno被设置为EINTR),但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此以后进行检查以查看连接是否成功建立
  4. 与accept调用一样,connect调用的阻塞特性可以通过该文件描述符的O_NONBLOCK标志来改变.此时,如果连接不能立刻建立,connect将失败并把errno设置为EINPROGRESS,而连接将以异步的方式继续进行
  5. 虽然异步连接难于处理,但我们可以在套接字文件描述符上,用select调用来检查套接字是否已处于写就绪状态

15.2.8 关闭套接字

可以通过调用close函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样,你应该总是在连接的两端都关闭套接字.对于服务器来说,应该在read调用返回0时关闭套接字,但如果套接字是一个面向连接类型的,并且设置了SOCK_LINGER选项,close调用会在该套接字还有未传输数据时阻塞

15.2.9 套接字通信

我们将在局域网中运行我们的客户和服务器,但网络套接字不仅可用于局域网,任何带有因特网连接的机器都可以使用网络套接字来彼此通信.甚至可以在一套UNIX单机上运行基于网络的程序,因为UNIX计算机通常都会配置了一个只包含它自身的回路网络.回路网络中只包含一台计算机,传统上它被称为localhost,他有一个标准的IP地址127.0.0.1.这就是本地主机

网络客户: client2.c

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>int main(){    int sockfd;    int len;    struct sockaddr_in address;    int result;    char ch = 'A';    sockfd = socket(AF_INET, SOCK_STREAM, 0);    address.sin_family = AF_INET;    address.sin_addr.s_addr = inet_addr("127.0.0.1");    address.sin_port = 9734;    len = sizeof(address);    result = connect(sockfd, (struct sockaddr *)&address, len);    if(result == -1){        perror("oops: client2 failed.");        exit(1);    }    write(sockfd, &ch, 1);    read(sockfd, &ch, 1);    printf("char form server = %c.\n", ch);    close(sockfd);    exit(0);}

网络服务器:server2.c

#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>int main(){    int server_sockfd, client_sockfd;    int server_len, client_len;    struct sockaddr_in server_address;    struct sockaddr_in client_address;    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);    server_address.sin_family = AF_INET;    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");    server_address.sin_port = 9734;    server_len = sizeof(server_address);    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);    listen(server_sockfd, 5);    while(1){        char ch;        printf("server waiting.\n");        client_len = sizeof(client_address);        client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len);        read(client_sockfd, &ch, 1);        ch++;        write(client_sockfd, &ch, 1);        close(client_sockfd);    }    exit(0);}

15.2.10 主机字节序和网络字节序

netstat -A inet

可以看到显示的本地地址(服务器套接字)的端口和你设置的套接字的端口不一样.因为,通过套接字接口传递的地址都是二进制数字.不同的计算机使用不同的字节序来表示整数,为了使不同类型的计算机可以通过网络传输的多字节整数值达成一致,你需要定义一个网络字节序,客户和服务器程序必须在传输之前,将他们内部的整数表示方式转换为网络字节序.他们通过定义在头文件netinet/in.h中的函数来完成这一工作

#include <netinet/in.h>unsigned long int htonl(unsigned long int hostlong);unsigned short int htons(unsigned short int hostshort);unsigned long int ntohl(undigned long int netlong);undigned short int ntohs(unsigned short int netshort);
  1. 这些函数将16位和32为整数在主机字节序和标准的网络字节许之间进行转换.函数名是与之对应的操作的简写形式.例如”host to network, long”(htonl,长整数从主机字节序到网络字节序的转换)和”network to host, short”(ntohs, 短整数从网络字节序到主机字节序的转换),如果计算机本身的主机字节序与网络字节序相同,这些函数的内容实际上就是空操作
  2. 为了保证16位的端口号有正确的字节序,你的服务器和客户端需要用浙西额函数来转换端口地址.新服务器程序server3.c中的改动是:
     
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9734);
  3. 你不需要对函数调用inet(“127.0.0.1”)进行转换,因为inet_addr已被定义为产生一个网络字节序的结果.新客户程序client3.c中的改动是:
     
    address.sin_port = htons(9734);

网络客户:client3.c

网络服务器:server3.c

15.3 网络信息

到目前为止,我们 客户和服务器程序一直是把地址和端口号编译到他们自己的内部.对于一个更通用的服务器和客户端程序来说,我们可以通过网络信息函数来决定应该使用的地址和端口

#include <netdb.h>struct hostent *gethostbyaddr(const void *addr, size_t len, int type);struct hostent *gethostbyname(const char *name);
  1. 这些函数返回的结构中至少会包含以下几个成员:
     
    struct hostent {
    char h_name; / name of the host */
    char *h_aliases; / list of aliases */
    int h_addrtype; /* address type */
    int h_length; /* length in bytes of the address */
    char *h_addr_list; / list of address (network order) */
    }
  2. 如果没有与我们查询的主机或地址相关的数据项,这些信息函数将返回一个空指针

类似的,与服务及其关联端口号有关的信息也可以通过一些服务信息函数来获取

#include <netdb.h>struct servent *getservbyname(const char *name, const char *proto);struct servent *getservbyport(int port, const char *proto);
  1. proto参数指定用于连接该服务的协议,它的两个取值是tcp和udp,前者用于SOCK_STREAM类型的TCP连接,后者用于SOCK_DGRAM类型的UDP数据报
  2. 结构servent至少包含以下几个成员:
     
    struct servent {
    char s_name; / name of the service */
    char *s_aliases / list of aliases (alternative names) */
    int s_port; /* The IP port number */
    char s_proto; / The server type, usually "tcp" or "udp" */
    }

如果想获得某台计算机的主机数据库信息,可以调用gethostbyname函数并且将结果打印出来.注意,要把返回的地址列表转换为正确的地址类型,并用函数inet_ntoa将他们从网络字节序转换为可以打印的字符串.函数inet_ntoa的定义如下所示

#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
  1. 这个函数的作用是,将一个因特网主机地址转换为一个点分四元组格式的字符串.他在失败时返回-1.

其他可用的新函数还有gethostname,它的定义如下所示

#include <unistd.h>int gethostname(char *name, int namelength);
  1. 这个函数的作用是将当前主机的名字写入name指向的字符串中.主机名将以null结尾.参数namelength指定了字符串name的长度,如果返回的主机名太长,他就会被截断.
  2. gethostname在成功时返回0,失败时返回-1.

网络信息

#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <netdb.h>int main(int argc, char *argv[]){    char *host, **names, **addrs;    struct hostent *hostinfo;    if(argc == 1){        char myname[256];        gethostname(myname, 255);        host = myname;    }    else      host  = argv[1];    hostinfo = gethostbyname(host);    if(!hostinfo){        fprintf(stderr, "cannot get info for host: %s\n", host);        exit(1);    }    printf("result for host %s:\n", host);    printf("Name: %s\n", hostinfo->h_name);    printf("Aliases:");    names = hostinfo->h_aliases;    while(*names){        printf(" %s", *names);        names++;    }    printf("\n");    if(hostinfo->h_addrtype != AF_INET){        fprintf(stderr, "Not a IP host!\n");        exit(1);    }    addrs = hostinfo->h_addr_list;    while(*addrs){        printf("%s ", inet_ntoa(*(struct in_addr *)*addrs));        addrs++;    }    printf("\n");    exit(0);}

连接到标准服务

#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <netinet/in.h>#include <netdb.h>int main(int argc, char *argv[]){    char *host;    int sockfd;    int len, result;    struct sockaddr_in address;    struct hostent *hostinfo;    struct servent *servinfo;    char buffer[128];    if(argc == 1)      host = "localhost";    else      host = argv[1];    hostinfo = gethostbyname(host);    if(!hostinfo){        fprintf(stderr, "no host: %s\n", host);        exit(1);    }    servinfo = getservbyname("daytime", "tcp");    if(!servinfo){        fprintf(stderr, "no daytime service\n");        exit(1);    }    printf("daytime port is %d\n", ntohs(servinfo->s_port));    sockfd = socket(AF_INET, SOCK_STREAM, 0);    address.sin_family = AF_INET;    address.sin_port = servinfo->s_port;    address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list;    len = sizeof(address);    result = connect(sockfd, (struct sockaddr *)&address, len);    if(result == -1){        perror("oops: getdate");        exit(1);    }    result = read(sockfd, buffer, sizeof(buffer));    buffer[result] = '\0';    printf("read %d bytes: %s", result, buffer);    close(sockfd);    exit(0);}

15.3.1 因特网守护进程(xinetd/inetd)

UNIX系统通常以超级服务器的方式来提供多项网络服务.超级服务器程序(因特网守护进程xinetd或者inetd)同时监听许多端口地址上的连接.当客户连接到某项服务时,守护程序就运行相应的服务器.这使得针对各项网络服务的服务器不需要一直运行着,他们可以在需要时启动.它的配置文件通常是/etc/xinetd.conf和/etc/xinetd.d目录中的文件

每一个由xinetd提供的服务都在/etc/xinet.d目录中有一个对应的配置文件.xinetd将在其启动时或被要求的情况下读取所有这些配置文件

15.3.2 套接字选项

setsockopt函数用于控制套接字选项

#include <sys/socket.h>int setsockopt(int socket, int level, int option_name,        const void *option_value, size_t option_len);
  1. 可以在协议层次的不同级别对选项进行设置.如果想要在套接字级别设置选项,你就必须将level参数设置为SOL_SCOKET.如果想要在底层协议级别(如TCP,UDP等)设置选项,就必须将level参数设置为该协议的编号(可以通过头文件netinet/in.h或者getprotobyname来获得)
  2. option_name参数指定要设置的选项;
  3. option_value参数的长度为option_len字节,它用于设置选项的新值,他被传递给底层协议的处理函数,并且不能被修改
  4. 在头文件sys/socket.h中定义的套接字级别选项如下:
     
    SO_DEBUG : 打开调试信息
    SO_KEEPALIVE : 通过定期传输保持存活报文来维持连接
    SO_LINGER : 在close调用返回之前完成传输工作
  5. SO_DEBUG和SO_KEEPALIVE用一个整数的option_value值来设置该选项的开(1)或关(0).
  6. SO_LINGER需要使用一个在头文件sys/socket.h中定义的linger结构,来定义该选项的状态以及套接字关闭之前的拖延时间
  7. setsockopt在成功时返回0,失败时返回-1

15.4 多用户

服务器程序在接受来自客户的一个新连接时会创建一个新的套接字,而原先的监听套接字将被保留以继续监听以后的连接.如果服务器不能立即接受后来的连接,他们将被放到队列中以等待处理

原先的套接字仍然可用并且套接字的行为就像文件描述符这一事实给我们提供了一种同时服务多个客户的方法.如果服务器调用fork为自己创建第二份副本,打开的套接字就将被新的子进程所继承.新的子进程可以和连接的客户进行连接,而主服务器进程可以继续接受以后的客户连接

因为我们创建子进程,但并不等待他们的完成,所以必须安排服务器设置SIGCHLD信号以避免出现僵尸进程

可以同时服务多个客户的服务器

#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <signal.h>int main(){    int server_sockfd, client_sockfd;    int server_len, client_len;    struct sockaddr_in server_address;    struct sockaddr_in client_address;    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);    server_address.sin_family = AF_INET;    server_address.sin_addr.s_addr = htonl(INADDR_ANY);    server_address.sin_port = htons(9734);    server_len = sizeof(server_address);    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);    listen(server_sockfd, 5);    signal(SIGCHLD, SIG_IGN);    while(1){        char ch;        printf("server wating...\n");        client_len = sizeof(client_address);        client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, (socklen_t *)&client_len);        if(fork() == 0){            read(client_sockfd, &ch, 1);            sleep(20);            ch++;            write(client_sockfd, &ch, 1);            close(client_sockfd);            exit(0);        }        else            close(client_sockfd);    }}

15.4.1 select系统调用

select系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或者输出的完成).这意味着终端仿真程序可以一直阻塞到有事情可做为止.类似的,服务器也可以通过同时在多个打开的套接字上等待请求到来的方法来处理多个用户

select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合.有一组已经定义好的宏可以用来控制这些集合

#include <sys/types.h>#include <sys/time.h>void FD_ZERO(fd_set *fdset);void FD_CLR(int fd, fd_set *fdset);void FD_SET(int fd, fd_set *fdset);void FD_ISSET(int fd, fd_set *fdset);
  1. FD_ZERO用于将fd_set初始化为空集合,FD_SET和FD_CLR分别用于在集合中设置和清除有参数fd传递的文件描述符.
  2. 如果FD_ISSET宏中有参数fd指向的文件描述符是由参数fdset指向的fd_set集合中的一个元素,FD_ISSET将返回非零值.
  3. fd_set结构中可以容纳的文件描述符的最大数目由常量FD_SETSIZE指定.
  4. select函数还可以用一个超时值来防止无限期的阻塞.这个超时值由一个timeval结构给出.这个结构定义在头文件sys/time.h中,它由以下几个成员组成:
     
    struct timeval {
    time_t tv_sec; /* seconds */
    long tv_usec; /* microseconds */
    };

select系统调用的原型如下:

#include <sys/types.h>#include <sys/time.h>int select(int nfds, fd_set *readfds, fd_set *writefds,           fd_set *errorfds, struct timeval *timeout);
  1. select调用用于测试文件描述符集合中是否有一个文件描述符已处于可读状态或可写状态或错误状态,他将以阻塞以等待某个文件描述符进入上述状态
  2. 参数nfds指定需要测试的文件描述符数目,测试的描述符范围从0~(nfds-1).
  3. 三个文件描述符集合都可以被设置为空指针,则表示不执行相应的测试
  4. select函数会在发生以下情况时返回:readfds集合中有描述符可读,writefds中有描述符可写,或errorfds集合中有描述符遇到错误条件.如果这三种情况都没有发生,select将在timeout指定的超时时间经过后返回.如果timeout参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下去
  5. 当select返回时,描述符集合将被修改以指示哪些描述符正处于可读,可写或有错误状态.我们可以用FD_ISSET对描述符进行测试,来找出需要注意的描述符.如果select是因为超时而被返回的话所有描述符集合都会被清空
  6. select调用返回状态发生变化的描述符总数.失败时他将返回-1并设置errno来描述错误.可能出现的错误有:EBADF(无效描述符).EINTR(因中断而返回),EINVAL(nfds或timeout取值错误)

select系统调用

#include <sys/types.h>#include <sys/time.h>#include <sys/ioctl.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <fcntl.h>int main(){    char buffer[128];    int result, nread;    fd_set inputs, testfds;    struct timeval timeout;    FD_ZERO(&inputs);    FD_SET(0, &inputs);    while(1){        testfds = inputs;        timeout.tv_sec = 2;        timeout.tv_usec = 500000;        result = select(FD_SETSIZE, &testfds, (fd_set *)NULL, (fd_set *)NULL, &timeout);        switch(result){            case 0:                printf("timeout...\n");                break;            case -1:                perror("select");                exit(1);            default:                if(FD_ISSET(0, &testfds)){                    ioctl(0, FIONREAD, &nread);                    if(nread == 0){                        printf("keyboard done\n");                        exit(0);                    }                    nread = read(0, buffer, nread);                    buffer[nread] = 0;                    printf("read %d from keyboard: %s", nread, buffer);                }                break;        }    }}

15.4.2 多客户

服务器可以让select调用同时监听套接字和客户的连接套接字.一旦select调用指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个上面有活动发生

一个改进的多客户/服务器

#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <sys/time.h>#include <sys/ioctl.h>int main(){    int server_sockfd, client_sockfd;    int server_len, client_len;    struct sockaddr_in server_address;    struct sockaddr_in client_address;    fd_set readfds, testfds;    int result;    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);    server_address.sin_family = AF_INET;    server_address.sin_addr.s_addr = htonl(INADDR_ANY);    server_address.sin_port = htons(9734);    server_len = sizeof(server_address);    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);    listen(server_sockfd, 5);    FD_ZERO(&readfds);    FD_SET(server_sockfd, &readfds);    while(1){        char ch;        int fd;        int nread;        testfds = readfds;        printf("server waiting...\n");        result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);        if(result < 1){            perror("server5");            exit(1);        }        for(fd = 0; fd < FD_SETSIZE; fd++){            if(FD_ISSET(fd, &testfds)){                if(fd == server_sockfd){                    client_len = sizeof(client_address);                    client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address,(socklen_t *)&client_len);                    FD_SET(client_sockfd, &readfds);                    printf("adding client on fd %d.\n", client_sockfd);                }                else {                    ioctl(fd, FIONREAD, &nread);                    if(nread == 0){                        close(fd);                        FD_CLR(fd, &readfds);                        printf("removing client on fd %d.\n", fd);                    }                    else {                        read(fd, &ch, 1);                        sleep(5);                        printf("serving client on fd %d.\n", fd);                        ch++;                        write(fd, &ch, 1);                    }                }            }        }    }}

15.5 数据报

当客户需要发送一个短小的查询请求给服务器,并且期望接收到一个短小的响应时,我们一般就使用有UDP提供的服务.如果服务器处理客户请求的时间足够短,服务器就可以通过一次处理一个客户请求的方式来提供服务,从而允许操作系统将客户的请求放入队列.这简化了服务器程序的编写

为了访问由UDP提供的服务,你需要像以前一样使用套接字和close系统调用,但你需要用两个数据报专用的系统调用sendto和recvfrom来代替原来使用在套接字上的read和write调用

sendto系统调用从buffer缓冲区中给使用指定套接字地址的目标服务器发送一个数据报,它的原型如下

int sendto(int sockfd, void *buffer, size_t len, int flag,        struct sockaddr *to, socklen_t tolen);
  1. 在正常应用中flag参数一般被设置为0

recvfrom系统调用在套接字上等待从特定地址到来的数据报,并将它放入buffer缓冲区,它的原型如下

int recvfrom(int sockfd, void *buffer, size_t len, int flags,        struct sockaddr *from, socklen_t *fromlen);
  1. 同样,在正常应用中,flags参数一般设为0
  2. 除非fcntl将套接字设置为非阻塞方式,否则,recvfrom调用将一直阻塞.我们可以用与前面的面向连接服务器一样的方式,通过select调用和超时设置来判断是否有数据到达套接字.
  3. 此外,还可以用alarm时钟信号来终端一个接收操作

getdate.c

#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <netinet/in.h>#include <netdb.h>int main(int argc, char *argv[]){    char *host;    int sockfd;    int len, result;    struct sockaddr_in address;    struct hostent *hostinfo;    struct servent *servinfo;    char buffer[128];    if(argc == 1)      host = "localhost";    else      host = argv[1];    hostinfo = gethostbyname(host);    if(!hostinfo){        fprintf(stderr, "no host: %s\n", host);        exit(1);    }    servinfo = getservbyname("daytime", "udp");    if(!servinfo){        fprintf(stderr, "no daytime service\n");        exit(1);    }    printf("daytime port is %d\n", ntohs(servinfo->s_port));    sockfd = socket(AF_INET, SOCK_DGRAM, 0);    address.sin_family = AF_INET;    address.sin_port = servinfo->s_port;    address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list;    len = sizeof(address);    result = sendto(sockfd, buffer, 1, 0, (struct sockaddr *)&address, len);    result = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&address, &len);    buffer[result] = '\0';    printf("read %d bytes: %s", result, buffer);    close(sockfd);    exit(0);}
0 0