UNIX网络编程从零起步(2): 交互式(interactive)服务器端

介绍完客户端程序后, 现在开始从最基本的交互式服务器端开始讲解. 所谓交互式服务就是在某个时刻只能处理一个连接. 服务器端主程序很简单:
server1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "server.h"
int
main(int argc, char *argv[])
{
    int lfd, cfd, fd;
    char *logpath = (char *) malloc(PATHSIZE);
    char *logbuf = (char *) malloc(LOGSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
 
    ssize_t n;
 
    if (argc == 1) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "5000");
    } else if (argc == 2) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "%s", argv[1]);
    } else if (argc == 3) {
        snprintf(logpath, PATHSIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
    } else {
        fprintf(stderr, "Usage:\n");
        fprintf(stderr, "%s [log file path] [server port]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((fd = open_logfile(logpath)) < 0) {
        fprintf(stderr, "open_logfd error.\n");
        exit(EXIT_FAILURE);
    }
 
    if ((lfd = tcp_listen(NULL, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_listen error.\n");
        exit(EXIT_FAILURE);
    }
 
 
    for (;;) {
        cfd = accept(lfd, NULL, NULL);
        for (; ;) {
            memset(logbuf, '\0', LOGSIZE);
            n = readn(cfd, logbuf, LOGSIZE);
            if (n != LOGSIZE && n != 0) {
                fprintf(stderr, "readn error.\n");
                break;
            } else if (n == 0) {
                fprintf(stderr, "connection is closed.\n");
                break;
            } else { 
                write(fd, logbuf, strlen(logbuf));
            }
            close(cfd);
        }        
    }
    free(logbuf);
    free(logpath);
    free(servname);
    return 0;
}

同样, 为了程序与协议无关, 在server1.c中第32行调用自己编写的tcp_listen函数. 这个函数的实现见tcp_connet.c, 它调用getaddrinfo(22行), 根据获得的地址信息完成TCP建立连接之前的调用socket(29行), bind(33行)和listen(46行)这三个系统调用的准备工作. 在第32行我们添加侦听端口lfd选项为SO_REUSEADDR. 这个属性对于不同的传输层协议有不同的含义. 对于TCP侦听端口来说它的主要用途包括两个方面: (1)服务器端程序重启, 但是该程序创建的子进程还在处理连接, 如果不设置这个选项重启时调用bind会失败; (2)如果服务器端有多个IP, 每个IP都使用了相同的端口对外提供服务, 如果不设置这个选项, 从启用第二个IP开启服务开始的bind都会失败. 为了程序更为健壮和适应更多类型的网络环境, 建议只要是使用TCP传输协议, 服务器端程序都应该给侦听端口添加SO_REUSEADDR.
tcp_listen.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "server.h"
 
int
tcp_listen(const char *hostname, const char *servname, SA *sockaddr, socklen_t *addrlen)
{
    struct addrinfo *ailist, *aip;
    struct addrinfo hint;
    const int optval = 1;
    int lfd, ret;
 
    memset(&hint, '\0', sizeof(struct addrinfo));
    hint.ai_flags = AI_CANONNAME;
    hint.ai_family = AF_UNSPEC;
    hint.ai_socktype = SOCK_STREAM;
    hint.ai_protocol = IPPROTO_TCP;
    hint.ai_flags = AI_PASSIVE;
    hint.ai_addrlen = 0;
    hint.ai_addr = NULL;
    hint.ai_canonname = NULL;
    hint.ai_next = NULL;
 
    if ((ret = getaddrinfo(hostname, servname, &hint, &ailist)) != 0) {
        fprintf(stderr, "getaddrinfo error.\n");
        return -1;
    }
 
    aip = ailist;
    do {
        lfd = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
        if (lfd < 0)
            continue;
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
        if (bind(lfd, aip->ai_addr, aip->ai_addrlen) == 0) {
            if (addrlen != NULL) {
                *addrlen = aip->ai_addrlen;
            }
            if (sockaddr != NULL && addrlen != NULL)
                memcpy(sockaddr, aip->ai_addr, *addrlen);
            break;
        }
        close(lfd);
    } while ((aip = aip->ai_next) != NULL);
 
    freeaddrinfo(ailist);
 
    if (listen(lfd, BACKLOG) < 0)
        return -1;
 
    return lfd;
}

和write一样, 用read从stream socket读取内容如果被信号中断, read不是返回-1而是返回已读取的字节并设置errno为EINTER. 除此之外, 当TCP接收缓冲区被填满时, 一次读取只能读取到缓冲区内的内容. 在我开始着手这个项目的时候我对TCP/IP不熟, 还不知道stream socket的这个特性. 那时我写程序从设备控制程序提供的端口读取设备状态信息, 发现不管我设置一次读取多少个字节, 总会有时候能够读取到所有状态信息, 有时只能返回部分信息. 后来仔细查阅书籍, 才发现遇到这种情况需要分多次读取. server1.c中41行readn的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
 
    ptr = vptr;
    nleft = n;
 
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
 
    return (n - nleft);
}

还需要一提的是server1.c中第25行open_logfile函数. 这个函数打开一个文件, 如果该文件不存在则创建一个文件.
open_logfile.c

1
2
3
4
5
6
7
8
#include "server.h"
int
open_logfile(const char *path)
{
    int fd;
    fd = open(path, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    return fd;
}

添加O_APPEND标志可以保证每次调用write都是原子的从文件末尾追加写入的内容. 在并发日志系统中非常重要.

编译serve1.c

gcc server1.c tcp_listen.c readn.c open_logfile.c -Wall -O2 -o serv1

运行serv1

./serv1

开启另外一个终端

netstat -na | grep 5000
tcp4 0 0 *.5000 *.* LISTEN

我们会发现端口5000处于侦听状态. 运行客户端程序./cli “Hello, this the first message!”, 在引号中的消息就会发送到当前目录下的server.log文件中. 至此, 我们完成了最基本的交互式服务器端.

此条目发表在计算机与网络技术分类目录,贴了, , 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据