笨辦法學C 練習45:一個簡單的TCP/IP客戶端

飛龍發表於2019-05-10

練習45:一個簡單的TCP/IP客戶端

原文:Exercise 45: A Simple TCP/IP Client

譯者:飛龍

我打算使用RingBuffer來建立一個非常簡單的小型網路測試工具,叫做netclient。為此我需要向Makefile新增一些工具,來處理bin/目錄下的小程式。

擴充套件Makefile

首先,為程式新增一些變數,就像單元測試的TESTSTEST_SRC變數:

PROGRAMS_SRC=$(wildcard bin/*.c)
PROGRAMS=$(patsubst %.c,%,$(PROGRAMS_SRC))

之後你可能想要新增PROGRAMS到所有目標中:

all: $(TARGET) $(SO_TARGET) tests $(PROGRAMS)

之後在clean目標中向rm那一行新增PROGRAMS

rm -rf build $(OBJECTS) $(TESTS) $(PROGRAMS)

最後你還需要在最後新增一個目標來構建它們:

$(PROGRAMS): CFLAGS += $(TARGET)

做了這些修改你就能夠將.c檔案扔到bin中,並且編譯它們以及為其連結庫檔案,就像測試那樣。

netclient 程式碼

netclient的程式碼是這樣的:

#undef NDEBUG
#include <stdlib.h>
#include <sys/select.h>
#include <stdio.h>
#include <lcthw/ringbuffer.h>
#include <lcthw/dbg.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>

struct tagbstring NL = bsStatic("
");
struct tagbstring CRLF = bsStatic("
");

int nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    check(flags >= 0, "Invalid flags on nonblock.");

    int rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    check(rc == 0, "Can`t set nonblocking.");

    return 0;
error:
    return -1;
}

int client_connect(char *host, char *port)
{
    int rc = 0;
    struct addrinfo *addr = NULL;

    rc = getaddrinfo(host, port, NULL, &addr);
    check(rc == 0, "Failed to lookup %s:%s", host, port);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    check(sock >= 0, "Cannot create a socket.");

    rc = connect(sock, addr->ai_addr, addr->ai_addrlen);
    check(rc == 0, "Connect failed.");

    rc = nonblock(sock);
    check(rc == 0, "Can`t set nonblocking.");

    freeaddrinfo(addr);
    return sock;

error:
    freeaddrinfo(addr);
    return -1;
}

int read_some(RingBuffer *buffer, int fd, int is_socket)
{
    int rc = 0;

    if(RingBuffer_available_data(buffer) == 0) {
        buffer->start = buffer->end = 0;
    }

    if(is_socket) {
        rc = recv(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer), 0);
    } else {
        rc = read(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer));
    }

    check(rc >= 0, "Failed to read from fd: %d", fd);

    RingBuffer_commit_write(buffer, rc);

    return rc;

error:
    return -1;
}


int write_some(RingBuffer *buffer, int fd, int is_socket)
{
    int rc = 0;
    bstring data = RingBuffer_get_all(buffer);

    check(data != NULL, "Failed to get from the buffer.");
    check(bfindreplace(data, &NL, &CRLF, 0) == BSTR_OK, "Failed to replace NL.");

    if(is_socket) {
        rc = send(fd, bdata(data), blength(data), 0);
    } else {
        rc = write(fd, bdata(data), blength(data));
    }

    check(rc == blength(data), "Failed to write everything to fd: %d.", fd);
    bdestroy(data);

    return rc;

error:
    return -1;
}


int main(int argc, char *argv[])
{
    fd_set allreads;
    fd_set readmask;

    int socket = 0;
    int rc = 0;
    RingBuffer *in_rb = RingBuffer_create(1024 * 10);
    RingBuffer *sock_rb = RingBuffer_create(1024 * 10);

    check(argc == 3, "USAGE: netclient host port");

    socket = client_connect(argv[1], argv[2]);
    check(socket >= 0, "connect to %s:%s failed.", argv[1], argv[2]);

    FD_ZERO(&allreads);
    FD_SET(socket, &allreads);
    FD_SET(0, &allreads);

    while(1) {
        readmask = allreads;
        rc = select(socket + 1, &readmask, NULL, NULL, NULL);
        check(rc >= 0, "select failed.");

        if(FD_ISSET(0, &readmask)) {
            rc = read_some(in_rb, 0, 0);
            check_debug(rc != -1, "Failed to read from stdin.");
        }

        if(FD_ISSET(socket, &readmask)) {
            rc = read_some(sock_rb, socket, 0);
            check_debug(rc != -1, "Failed to read from socket.");
        }

        while(!RingBuffer_empty(sock_rb)) {
            rc = write_some(sock_rb, 1, 0);
            check_debug(rc != -1, "Failed to write to stdout.");
        }

        while(!RingBuffer_empty(in_rb)) {
            rc = write_some(in_rb, socket, 1);
            check_debug(rc != -1, "Failed to write to socket.");
        }
    }

    return 0;

error:
    return -1;
}

程式碼中使用了select來處理stdin(檔案描述符0)和用於和伺服器互動的socket中的事件。它使用了RingBuffer來儲存和複製資料,並且你可以認為read_somewrite_some函式都是RingBuffer中相似函式的原型。

在這一小段程式碼中,可能有一些你並不知道的網路函式。當你碰到不知道的函式時,在手冊頁上查詢它來確保你理解了它。這一小段程式碼可能需要讓你研究用於小型伺服器程式設計的所有C語言API。

你會看到什麼

如果你完成了所有構建,測試的最快方式就是看看你能否從learncodethehardway.org上得到一個特殊的檔案:

$
$ ./bin/netclient learncodethehardway.org 80
GET /ex45.txt HTTP/1.1
Host: learncodethehardway.org

HTTP/1.1 200 OK
Date: Fri, 27 Apr 2012 00:41:25 GMT
Content-Type: text/plain
Content-Length: 41
Last-Modified: Fri, 27 Apr 2012 00:42:11 GMT
ETag: 4f99eb63-29
Server: Mongrel2/1.7.5

Learn C The Hard Way, Exercise 45 works.
^C
$

這裡我所做的事情是鍵入建立/ex45.txt的HTTP請求所需的語法,在Host:請求航之後,按下ENTER鍵來輸入空行。接著我獲取相應,包括響應頭和內容。最後我按下CTRL-C來退出。

如何使它崩潰

這段程式碼肯定含有bug,但是當前在本書的草稿中,我會繼續完成它。與此同時,嘗試分析程式碼,並且用其它伺服器來擊潰它。一種叫做netcat的工具可以用於建立這種伺服器。另一種方法就是使用PythonRuby之類的語言建立一個簡單的“垃圾伺服器”,來產生垃圾資料,隨機關閉連線,或者其它異常行為。

如果你找到了bug,在評論中報告它們,我會修復它。

附加題

  • 像我提到的那樣,這裡面有一些你不知道的函式,去查詢他們。實際上,即使你知道它們也要查詢。

  • valgrind下執行它來尋找錯誤。

  • 為函式新增各種防禦性程式設計檢查,來改進它們。

  • 使用getopt函式,執行使用者提供選項來防止將
    轉換為
    。這僅僅用於需要處理行尾的協議例如HTTP。有時你可能不想執行轉換,所以要給使用者一個選擇。

相關文章