# 練習45:一個簡單的TCP/IP客戶端
> 原文:[Exercise 45: A Simple TCP/IP Client](http://c.learncodethehardway.org/book/ex45.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
我打算使用`RingBuffer`來創建一個非常簡單的小型網絡測試工具,叫做`netclient`。為此我需要向`Makefile`添加一些工具,來處理`bin/`目錄下的小程序。
## 擴展Makefile
首先,為程序添加一些變量,就像單元測試的`TESTS`和`TEST_SRC`變量:
```Makefile
PROGRAMS_SRC=$(wildcard bin/*.c)
PROGRAMS=$(patsubst %.c,%,$(PROGRAMS_SRC))
```
之后你可能想要添加`PROGRAMS`到所有目標中:
```makefile
all: $(TARGET) $(SO_TARGET) tests $(PROGRAMS)
```
之后在`clean`目標中向`rm`那一行添加`PROGRAMS`:
```makefile
rm -rf build $(OBJECTS) $(TESTS) $(PROGRAMS)
```
最后你還需要在最后添加一個目標來構建它們:
```makefile
$(PROGRAMS): CFLAGS += $(TARGET)
```
做了這些修改你就能夠將`.c`文件扔到`bin`中,并且編譯它們以及為其鏈接庫文件,就像測試那樣。
## netclient 代碼
netclient的代碼是這樣的:
```c
#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("\n");
struct tagbstring CRLF = bsStatic("\r\n");
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_some`和`write_some`函數都是`RingBuffer`中相似函數的原型。
在這一小段代碼中,可能有一些你并不知道的網絡函數。當你碰到不知道的函數時,在手冊頁上查詢它來確保你理解了它。這一小段代碼可能需要讓你研究用于小型服務器編程的所有C語言API。
## 你會看到什么
如果你完成了所有構建,測試的最快方式就是看看你能否從learncodethehardway.org上得到一個特殊的文件:
```sh
$
$ ./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`的工具可以用于建立這種服務器。另一種方法就是使用`Python`或`Ruby`之類的語言創建一個簡單的“垃圾服務器”,來產生垃圾數據,隨機關閉連接,或者其它異常行為。
如果你找到了bug,在評論中報告它們,我會修復它。
## 附加題
+ 像我提到的那樣,這里面有一些你不知道的函數,去查詢他們。實際上,即使你知道它們也要查詢。
+ 在`valgrind`下運行它來尋找錯誤。
+ 為函數添加各種防御性編程檢查,來改進它們。
+ 使用`getopt`函數,運行用戶提供選項來防止將`\n`轉換為`\r\n`。這僅僅用于需要處理行尾的協議例如HTTP。有時你可能不想執行轉換,所以要給用戶一個選擇。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死