UNIX网络编程从零起步(1): 客户端程序

由于项目的需要, 从事Linux网络编程已有两年多的时间. 将来从事什么还未确定, 所以趁着在这个项目即将结束的时候把在网络编程的实践中学到的东西总结一下. 虽然标题是”从零起步”, 但并不是说零基础起步, 而是从最简单的交互式服务器端开始, 逐渐增加功能到复杂的支持并发连接的服务器端.

基于TCP/IP协议的网络编程, 整体基本框架大致是: 服务器端程创建一个socket, 然后将制定的端口号绑定到这个socket上, 接下来侦听这个socket. 当客户端发起连接时, accept返回一个连接socket并通过此连接socket和远端的客户端通讯. 客户端则是创建一个socket, 将socket连接到服务器端, 通过此socket与服务器端通讯.

在我编写项目程序的过程中发现, 由于很多进程都需要开机自动启动运行, 为了监控和追溯设备运行状况, 必须要把每一步的操作都记录在日志文件里. 但是Unix系统提供的syslog并不能很好的满足我的需求, 于是单独写了一个简单的基于网络通讯的日志记录程序. 这个程序功能简单, 没有复杂的业务逻辑, 正好适合当总结网络编程的例子. 网络程序编写之前需要定好通讯协议, 日志记录程序的通讯协议也很简单, 客户端向服务器端发送事先约定好长度的定长字符串, 服务器端接收并把字符串内容写到文件中.

头文件server.h

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
#ifndef _SERVER_H
#define _SERVER_H 1
 
#define PATHSIZE        256
#define HOSTNAMESIZE    256
#define SERVNAMESIZE    16
#define LOGSIZE         4096
#define BACKLOG         10
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
 
typedef struct sockaddr SA;
 
int open_logfile(const char *);
ssize_t readn(int, void *, size_t);
int recv_log(int, char *);
int send_log(int, const char *);
int tcp_connect(const char *, const char *, SA *, socklen_t *);
int tcp_listen(const char *, const char *, SA *, socklen_t *);
ssize_t writen(int, const void *, size_t);
#endif

客户端程序client.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
#include "server.h"
 
int
main(int argc, char *argv[])
{
    int logfd;
    char *hostname = (char *) malloc(HOSTNAMESIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    char *logbuf = (char *) malloc(LOGSIZE);
 
    if (hostname == NULL || servname == NULL || logbuf == NULL) {
        fprintf(stderr, "malloc error.\n");
        exit(EXIT_FAILURE);
    }
 
    if (argc == 2) {
        snprintf(hostname, HOSTNAMESIZE, "localhost");
        snprintf(servname, SERVNAMESIZE, "5000");
        snprintf(logbuf, LOGSIZE, "%s\n", argv[1]);
    } else if (argc == 4) {
        snprintf(hostname, HOSTNAMESIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
        snprintf(logbuf, LOGSIZE, "%s\n", argv[3]);
    } else {
        fprintf(stderr, "Usage: \n");
        fprintf(stderr, "%s [hostname] [port] <message> \n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((logfd = tcp_connect(hostname, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_connect error.\n");
        exit(EXIT_FAILURE);
    }
 
    send_log(logfd, logbuf);
    close(logfd);
 
    free(logbuf);
    free(hostname);
    free(servname);
 
    exit(EXIT_SUCCESS);
}

目前IPV6已经在国内高校科研单位实验性部署, 让TCP连接与IP协议无关是一个”向前”兼容的好主意. tcp_connect函数创建socket并使用getaddrinfo系统调用创建协议无关连接.
tcp_connect.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
#include "server.h"
 
int
tcp_connect(const char *hostname, const char *servname, SA *sockaddr, socklen_t *addrlen)
{
    struct addrinfo *ailist, *aip;
    struct addrinfo hint;
    int sockfd;
    int ret;
 
    memset(&hint, '\0', sizeof(struct addrinfo));
    hint.ai_family = AF_UNSPEC;
    hint.ai_socktype = SOCK_STREAM;
 
    if ((ret = getaddrinfo(hostname, servname, &hint, &ailist)) != 0) {
        fprintf(stderr, "%s", gai_strerror(ret));
        return -1;
    }
 
    aip = ailist;
    do {
        sockfd = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
        if (sockfd < 0)
            continue;
        if (connect(sockfd, aip->ai_addr, aip->ai_addrlen) == 0) {
            if (addrlen != NULL)
                memcpy(addrlen, &aip->ai_addrlen, sizeof(socklen_t));
            if (sockaddr != NULL)
                memcpy(sockaddr, aip->ai_addr, aip->ai_addrlen);
            break;
        }
        close(sockfd);
    } while ((aip = aip->ai_next) != NULL);
 
    if (aip == NULL) {
        sockfd = -1;
    }
 
    freeaddrinfo(ailist);
 
    return sockfd;
}

发送日志函数send_log.c

1
2
3
4
5
6
7
8
9
10
11
12
#include "server.h"
int
send_log(int sockfd, const char *logbuf)
{
    ssize_t n;
    n = writen(sockfd, logbuf, LOGSIZE);
    if (n != LOGSIZE) {
        fprintf(stderr, "send_log error.\n");
        return -1;
    }
    return 0;
}

使用write系统调用往stream socket写入至少1个字节, 这时如果write的时候所在线程收到signal并调用signal handler, 在信号处理函数返回后, write并不是返回-1而是成功返回已发送字节数; 若往write在写入任何字节之前被信号中断, write返回-1并且设置errno为EINTER. 这种情况下, 我们应该多次调用直到发送完所有要发送的字节数为止. 因此, 我们用writen函数代替直接的write.
writen.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "server.h"
ssize_t
writen(int fd, const void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
 
    ptr = vptr;
    nleft = n; 
    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) < 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else 
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
 
    return n;
}

编译客户端程序

gcc client.c writen.c tcp_connet.c send_log.c -Wall -O2 -o cli

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

发表评论

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

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