练习45:一个简单的TCP/IP客户端

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

译者:飞龙

我打算使用RingBuffer来创建一个非常简单的小型网络测试工具,叫做netclient。为此我需要向Makefile添加一些工具,来处理bin/目录下的小程序。

扩展Makefile

首先,为程序添加一些变量,就像单元测试的TESTSTEST_SRC变量:

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

之后你可能想要添加PROGRAMS到所有目标中:

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

之后在clean目标中向rm那一行添加PROGRAMS

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

最后你还需要在最后添加一个目标来构建它们:

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

做了这些修改你就能够将.c文件扔到bin中,并且编译它们以及为其链接库文件,就像测试那样。

netclient 代码

netclient的代码是这样的:

  1. #undef NDEBUG
  2. #include <stdlib.h>
  3. #include <sys/select.h>
  4. #include <stdio.h>
  5. #include <lcthw/ringbuffer.h>
  6. #include <lcthw/dbg.h>
  7. #include <sys/socket.h>
  8. #include <sys/types.h>
  9. #include <sys/uio.h>
  10. #include <arpa/inet.h>
  11. #include <netdb.h>
  12. #include <unistd.h>
  13. #include <fcntl.h>
  14. struct tagbstring NL = bsStatic("\n");
  15. struct tagbstring CRLF = bsStatic("\r\n");
  16. int nonblock(int fd) {
  17. int flags = fcntl(fd, F_GETFL, 0);
  18. check(flags >= 0, "Invalid flags on nonblock.");
  19. int rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
  20. check(rc == 0, "Can't set nonblocking.");
  21. return 0;
  22. error:
  23. return -1;
  24. }
  25. int client_connect(char *host, char *port)
  26. {
  27. int rc = 0;
  28. struct addrinfo *addr = NULL;
  29. rc = getaddrinfo(host, port, NULL, &addr);
  30. check(rc == 0, "Failed to lookup %s:%s", host, port);
  31. int sock = socket(AF_INET, SOCK_STREAM, 0);
  32. check(sock >= 0, "Cannot create a socket.");
  33. rc = connect(sock, addr->ai_addr, addr->ai_addrlen);
  34. check(rc == 0, "Connect failed.");
  35. rc = nonblock(sock);
  36. check(rc == 0, "Can't set nonblocking.");
  37. freeaddrinfo(addr);
  38. return sock;
  39. error:
  40. freeaddrinfo(addr);
  41. return -1;
  42. }
  43. int read_some(RingBuffer *buffer, int fd, int is_socket)
  44. {
  45. int rc = 0;
  46. if(RingBuffer_available_data(buffer) == 0) {
  47. buffer->start = buffer->end = 0;
  48. }
  49. if(is_socket) {
  50. rc = recv(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer), 0);
  51. } else {
  52. rc = read(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer));
  53. }
  54. check(rc >= 0, "Failed to read from fd: %d", fd);
  55. RingBuffer_commit_write(buffer, rc);
  56. return rc;
  57. error:
  58. return -1;
  59. }
  60. int write_some(RingBuffer *buffer, int fd, int is_socket)
  61. {
  62. int rc = 0;
  63. bstring data = RingBuffer_get_all(buffer);
  64. check(data != NULL, "Failed to get from the buffer.");
  65. check(bfindreplace(data, &NL, &CRLF, 0) == BSTR_OK, "Failed to replace NL.");
  66. if(is_socket) {
  67. rc = send(fd, bdata(data), blength(data), 0);
  68. } else {
  69. rc = write(fd, bdata(data), blength(data));
  70. }
  71. check(rc == blength(data), "Failed to write everything to fd: %d.", fd);
  72. bdestroy(data);
  73. return rc;
  74. error:
  75. return -1;
  76. }
  77. int main(int argc, char *argv[])
  78. {
  79. fd_set allreads;
  80. fd_set readmask;
  81. int socket = 0;
  82. int rc = 0;
  83. RingBuffer *in_rb = RingBuffer_create(1024 * 10);
  84. RingBuffer *sock_rb = RingBuffer_create(1024 * 10);
  85. check(argc == 3, "USAGE: netclient host port");
  86. socket = client_connect(argv[1], argv[2]);
  87. check(socket >= 0, "connect to %s:%s failed.", argv[1], argv[2]);
  88. FD_ZERO(&allreads);
  89. FD_SET(socket, &allreads);
  90. FD_SET(0, &allreads);
  91. while(1) {
  92. readmask = allreads;
  93. rc = select(socket + 1, &readmask, NULL, NULL, NULL);
  94. check(rc >= 0, "select failed.");
  95. if(FD_ISSET(0, &readmask)) {
  96. rc = read_some(in_rb, 0, 0);
  97. check_debug(rc != -1, "Failed to read from stdin.");
  98. }
  99. if(FD_ISSET(socket, &readmask)) {
  100. rc = read_some(sock_rb, socket, 0);
  101. check_debug(rc != -1, "Failed to read from socket.");
  102. }
  103. while(!RingBuffer_empty(sock_rb)) {
  104. rc = write_some(sock_rb, 1, 0);
  105. check_debug(rc != -1, "Failed to write to stdout.");
  106. }
  107. while(!RingBuffer_empty(in_rb)) {
  108. rc = write_some(in_rb, socket, 1);
  109. check_debug(rc != -1, "Failed to write to socket.");
  110. }
  111. }
  112. return 0;
  113. error:
  114. return -1;
  115. }

代码中使用了select来处理stdin(文件描述符0)和用于和服务器交互的socket中的事件。它使用了RingBuffer来储存和复制数据,并且你可以认为read_somewrite_some函数都是RingBuffer中相似函数的原型。

在这一小段代码中,可能有一些你并不知道的网络函数。当你碰到不知道的函数时,在手册页上查询它来确保你理解了它。这一小段代码可能需要让你研究用于小型服务器编程的所有C语言API。

你会看到什么

如果你完成了所有构建,测试的最快方式就是看看你能否从learncodethehardway.org上得到一个特殊的文件:

  1. $
  2. $ ./bin/netclient learncodethehardway.org 80
  3. GET /ex45.txt HTTP/1.1
  4. Host: learncodethehardway.org
  5. HTTP/1.1 200 OK
  6. Date: Fri, 27 Apr 2012 00:41:25 GMT
  7. Content-Type: text/plain
  8. Content-Length: 41
  9. Last-Modified: Fri, 27 Apr 2012 00:42:11 GMT
  10. ETag: 4f99eb63-29
  11. Server: Mongrel2/1.7.5
  12. Learn C The Hard Way, Exercise 45 works.
  13. ^C
  14. $

这里我所做的事情是键入创建/ex45.txt的HTTP请求所需的语法,在Host:请求航之后,按下ENTER键来输入空行。接着我获取相应,包括响应头和内容。最后我按下CTRL-C来退出。

如何使它崩溃

这段代码肯定含有bug,但是当前在本书的草稿中,我会继续完成它。与此同时,尝试分析代码,并且用其它服务器来击溃它。一种叫做netcat的工具可以用于建立这种服务器。另一种方法就是使用PythonRuby之类的语言创建一个简单的“垃圾服务器”,来产生垃圾数据,随机关闭连接,或者其它异常行为。

如果你找到了bug,在评论中报告它们,我会修复它。

附加题

  • 像我提到的那样,这里面有一些你不知道的函数,去查询他们。实际上,即使你知道它们也要查询。
  • valgrind下运行它来寻找错误。
  • 为函数添加各种防御性编程检查,来改进它们。
  • 使用getopt函数,运行用户提供选项来防止将\n转换为\r\n。这仅仅用于需要处理行尾的协议例如HTTP。有时你可能不想执行转换,所以要给用户一个选择。