======Exercise 45: A Simple TCP/IP Client====== I'm going to use the RingBuffer to create a very simplistic little network testing tool called netclient. To do this I have to add some stuff to the Makefile to handle little programs in the bin/ directory. ======Augment The Makefile====== First, add a variable for the programs just like the unit tests TESTS and TEST_SRC variables: ======PROGRAMS_SRC=$(wildcard bin/*.c)====== ======PROGRAMS=$(patsubst %.c,%,$(PROGRAMS_SRC))====== Then you want to add the PROGRAMS to the all target: all: $(TARGET) $(SO_TARGET) tests $(PROGRAMS) Then add PROGRAMS to the rm line in the clean target: rm -rf build $(OBJECTS) $(TESTS) $(PROGRAMS) Finally you just need a target at the end to build them all: $(PROGRAMS): CFLAGS += $(TARGET) With these changes you can drop simple .c files into bin and make will build them and link them to the library just like the tests are done. ======The netclient Code====== The code for the little netclient looks like this: #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(b uffer), 0); } else { rc = read(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(b uffer)); } 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; } This code uses select to handle events from both stdin (file descriptor 0) and from the socket it uses to talk to a server. It uses RingBuffers to store the data and copy it around, and you can consider the functions read_some and write_some early prototypes for similar functions in the RingBuffer library. In this little bit of code are quite a few networking functions you may not know. As you hit a function you don't know, look it up in the man pages and make sure you understand it. This one little file could actually get you to research all the APIs required to write a little server in C. ======What You Should See====== If you have everything building then the quickest way to test it is see if you can get a special file off 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 $ What I did there is I type in the syntax needed to make the HTTP request for the file /ex45.txt, then the Host: header line, then hit ENTER to get an empty line. I then get the response, with headers and the content. After that I just hit CTRL-c to exit. ======How To Break It====== This code definitely could have bugs, and currently in the draft of the book I'm going to have to keep working on this. In the meantime, try analyzing the code I have here and thrashing it against other servers. There's a tool called netcat that is great for setting up these kinds of servers. Another thing to do is use a language like Python or Ruby to create a simple "junk server" that spews out junk and bad data, closes connections randomly, and other nasty things. If you find bugs, report them in the comments and I'll fix them up. ======Extra Credit====== * As I mentioned, there's quite a few functions you may not know, so look them up. In fact, look them all up even if you think you know them. * Run this under valgrind and look for errors. * Go back through and add various defensive programming checks to the functions to improve them. * Use the getopt function to allow the user to give this the option to not translate \n to \r\n. This is only needed on protocols that require it for line endings, like HTTP. Sometimes you don't want the translation, so give the user an option. Copyright (C) 2010 Zed. A. Shaw Credits