Date: Thu, 16 Nov 2000 14:35:46 -0800 From: Bakul Shah <bakul@bitblocks.com> To: "Nicolai Petri" <nicolai@petri.cc> Cc: freebsd-hackers@freebsd.org Subject: Re: Multithreaded tcp-server or non-blocking ? Message-ID: <200011162235.RAA27714@thunderer.cnchost.com> In-Reply-To: Your message of "Thu, 16 Nov 2000 11:38:14 %2B0100." <021501c04fb9$574f9030$6732a8c0@atomic.dk>
next in thread | previous in thread | raw e-mail | index | archive | help
> What's the best approach for a simple web-server(never more the 10 clients) > ? Is it using pthread and a thread per connection . Or to make a > non-blocking single thread server. Can people show me some simple examples > of the 2 techniques ? > > And what's the pro's and con's for the 2 methods ??? The simplest solution is to just fork a new process on accepting a new connection. A sample implementation is included at the end. This solution doesn't scale too well as a process is quite heavy weight and uses much more resources than the other two solutions. One variation avoids the cost of fork/exit per connection. The connection accepting process passes the new file descriptor in a message to a free server process. The acceptor creates server threads as needed and the server threads can be made to exit when they are idle for a while. The thread per connection solution is also quite simple and significantly more efficient that process per connection since a thread context switch is a lot cheaper than a process context switch. But you need to deal with concurrency issues if you have to share a bunch of state and your threads can be preempted. With this and the next solution you can also run into file descriptor limits. I don't have an example I can show you. A non-blocking single thread server is likely to be more portable than the pthread solution. See http://www.bitblocks.com/src/select-test.c for an example. The basic problem here is what to do if deep down in some function you need to wait for some response from a client. As an example, consider the case where function A is called when a new connection comes in. A calls B and B calls C and C wants to block. You have no choice but to implement this as a state machine and break functions B, C in B1, B2, C1 and C2 and carry around appropriate state. So you have to take something like A(int fd) { // called on a new connection ... B(fd); ... } B(int fd) { ... C(fd); ... } C(int fd) { ... read(fd, buf, len); ... } and change it to A(int fd, Context* c) { // called on a new connection switch (c->state) { case init: ... B1(fd, c); break; case more: B2(fd, c); ... } } B1(int fd, Context* c) { ... C1(fd, c); } B2(int fd, Context* c) { C2(fd, c); ... } C1(int fd, Context* c) { ... c->state = more; } C2(int fd, Context* c) { len = read(fd, bufptr, len); if (read enough) c->state = init; else update bufptr ... } If you have a complex interaction with the client, the number of states can explode. Worse, such interactions may get added as the application evolves so you are forced to do a conversion like the one shown above. In effect here you are implementing threading in a application specific way and the c->state variable has a role analogous to a program counter of a thread. At the same time concurrency issues are easier to deal with than the pthreads case since this is cooperative multithreading. In my (somewhat dated) experience this solution was a bit more efficient than the pthreads version but significantly more complex -- this situation may have changed in pthreads' favor. Also, typically here you require much less memory to store a connection's state than what you spend with a pthread -- an issue if you have thousands of connections. If only a few are active at a time this single thread select loop solution is a perfectly usable solution and uses the least resources. You didn't ask for this but note that for handling a very large number of clients (thousands to millions) you have to use a combination of techniques to deal with various resource limits. For example, you can push timeconsuming diskio requests to separate threads or ever separate machines. You can use multiple processes to deal with file descriptor limits and so on. -- bakul /* A simple fork-a-server-on-accepting-a-connection solution */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/wait.h> #include <signal.h> void reaper(int ignore) { int status; wait(&status); } void process_request(int fd) { char buffer[11]; int len = sprintf(buffer, "%d\n", getpid()); write(fd, buffer, len); } int main(int argc, char** argv) { u_short port = argc > 1? strtoul(argv[1], 0, 0) : 1234; int s = socket(PF_INET, SOCK_STREAM, 0); int one = 1; struct sockaddr_in addr; if (s < 0) { perror("socket"); exit(1); } bzero(&addr, sizeof addr); addr.sin_len = sizeof addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); if (bind(s, (struct sockaddr*)&addr, sizeof addr) < 0) { perror("bind"); exit(1); } if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one) < 0) { perror("setsockopt"); exit(1); } if (listen(s, 5) < 0) { perror("listen"); exit(1); } if (signal(SIGCHLD, reaper) < 0) { perror("signal"); exit(1); } for (;;) { int len = sizeof addr; int fd = accept(s, (struct sockaddr*)&addr, &len); int pid; if (fd < 0) continue; pid = fork(); if (pid) { /*child */ process_request(fd); exit(0); } /* parent */ close(fd); if (pid < 0) { perror("fork"); continue; } } } To Unsubscribe: send mail to majordomo@FreeBSD.org with "unsubscribe freebsd-hackers" in the body of the message
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?200011162235.RAA27714>