Valhalla Legends Forums Archive | C/C++ Programming | [C/Linux] Server Model, need help

AuthorMessageTime
rabbit
I'm writing a fairly simple server for chatting, mostly for fun and just to learn how to make something like this.
Right now, I'm thinking about something like this:
[img]http://www.liquid-server.org/images/model.png[/img]

Basically, I have the main body with all the intialization and stuff of that nature.  Elsewhere, I've got configuration handlers for loading/saving the configuration files; a settings system (mostly what I need help with); a data buffering system; and incoming and outgoing parsers (easy parts to make, still need a little help).

Basically, red arrows are raw data coming in, green arrows are links between parts of the program, and blue are necessary links I can't think of a way to describe concisely.  Basically, my problem is that I suck at C.  I want my config handlers to be able to modify variables in settings, though I'm not sure how to do this.  Wrappers, direct edits, what?  Also, how would my parsers be able to use variables and such from the settings portion?
June 14, 2007, 5:45 PM
Barabajagal
Use either global variables, or public functions that are just Get and Set of the variables you need to use publicly?
June 14, 2007, 6:11 PM
rabbit
I've always had trouble using global variables.  I'm not sure why.  Whenever I've used them in the past I've gotten errors about unknown variables and the such.  Always a pain.  I don't really want to use a wrapper, but if I have to I will..

[edit]
It appears that it's because I haven't been using extern...okay :D
June 14, 2007, 6:14 PM
iago
Using extern is generally evil. It should be avoided whenever possible.

Settings might be an exception for it. The way you should implement it is by putting the variable itself in a .c file, and the "extern" in the corresponding .h. That way, whenever something includes the .h file for the module, it gets access to that variable.
June 14, 2007, 6:53 PM
St0rm.iD
Just have ONE global variable called "gApp" which is a struct containing every global that you would need (including config settings).

The scariest part, which you haven't dealt with, is the concept of time (which is one of the scariest concepts in computing). How are you going to handle talking to multiple connections? Threads? Pseudo-threads? Nonblocking I/O? Callbacks?
June 15, 2007, 12:26 PM
rabbit
Well, Linux makes it much easier to multi-thread (fork() vs CreateProcess(1 billion args)), but I've also been looking at nonblocking I/O.  This is going to be a small server, with a user cap of maybe 100-200 connections.  I haven't decided on how I'm going to handle that stuff yet because I don't know much about it.  Suggestions on that?
June 15, 2007, 12:34 PM
iago
Figure out how to use select(). You can do a select() across any number of outgoing sockets and incoming sockets, as well as stdin.

If you want a demonstration of using select() for a chat server/client, let me know. I have one uploaded somewhere that wrote for school. I could find it if you want.
June 15, 2007, 1:09 PM
rabbit
That would be awesome :D

All the stuff I've been reading about select is kind of vague, and the code is weird :\
June 15, 2007, 2:43 PM
St0rm.iD
forking is NOT a good way to go. The One True Way to write networked applications (IMO) is a few worker processes (or real OS threads) which use non-blocking I/O to process everything (I believe this is how Apache does it). Usually, one process/thread per processor core is a good idea.
June 15, 2007, 5:30 PM
rabbit
So then maybe 1 thread for the socket, another for the data parsing, and a third for everything else?
June 15, 2007, 6:58 PM
iago
http://www.javaop.com/~ron/code/cc.tgz

Unless you're doing heavy processing, you only need 1 thread with non-blocking i/o (or using select()). CattleChat (nickname for the project) has a server and a client, both with a GUI (curses, text-based) and with exactly one thread each.

One thing you might find interesting is that I based my protocol on Battle.net, so it's a pretty good framework if anybody wants to hollow it out and change the packets to BNET ones. But that's neither here nor there. There's no license on this, it was a school project and I'm calling it public domain.
June 15, 2007, 8:16 PM
St0rm.iD
Best thing (IMO) is to have an I/O thread that is ONLY responsible for reading (and perhaps parsing) incoming data and stuffing it along with a client identifier (ip/port combo or socket handle) into a queue. It also reads from the outgoing data queue and does the appropriate I/O there as well. You can then have X worker threads in a pool waiting on that queue to actually act on that data. BE VERY CAREFUL ABOUT THREAD SYNCHRONIZATION. If you are careful about locking (or you just use message passing, perhaps with the queue), you should be alright.

To maintain client state, you're going to have a lookup table that maps a client ID (again, ip/port or handle) to a struct containing any data you need (is_logged_in, username, current_channel, etc).

Trust me, I know what I'm doing. This is the best way to do it. Why? When I said you can add X threads, it means you can just add more and more processors and, depending on how much shared state you have, your system will scale to more users. With multicore systems coming into play more and more, you're going to have to learn this eventually; learn it now. Sure, you could probably get away with just one thread for everything (or even one thread per client!), but that's not the right way to do it.

Do you know about proper locking techniques and what it means to have as little shared mutable state as possible? I love this stuff, and teaching someone else helps me learn more myself!
June 15, 2007, 9:18 PM
rabbit
I have no clue!  Please, teach me :D

I've always wanted to make a simple chat server of my own, but all attempts have thus far ended in tragedy: VB6, C, C++, and Java.  I got farthest in Java with data parsing and packet building, but nowhere with users.  In C and C++, I got all my file handling done pretty well, but nothing else.  My VB one actually was a full blown server, except it was prone to crash and was in VB.  BAH!

I really want to write some quality stuff this time.  It'd be great if you and Ron would help me out.
June 15, 2007, 11:25 PM
St0rm.iD
OK. Start at the beginning. Write an echo server: listens on a TCP port, accepts a connection, and whatever data it receives it sends back out. Do it with one thread and select() and nonblocking I/O. gogo~
June 16, 2007, 6:35 AM
rabbit
I'll get right on it!

An iago, could you please allow traffic through port 9933?  That'd be awesome.

select(): http://pastebin.com/930219
June 16, 2007, 12:56 PM
iago
He obviously knows more than I do. You can use my code there for reference (especially regarding how to use select()), but learn from him.

[quote author=rabbit link=topic=16786.msg170160#msg170160 date=1181998591]
An iago, could you please allow traffic through port 9933?  That'd be awesome.
[/quote]
Done.
June 16, 2007, 2:55 PM
rabbit
Okay, I put up my little echoer on the vm, but it won't run if I close my SSH.  How can I get around that?
June 16, 2007, 4:37 PM
iago
Learn how to use "screen".

When you log in, run "screen -r" (resume). If there's no screens to resume, run "screen". That'll open one. You can run stuff inside there and close your ssh client. Stuff will keep running, and you can use "screen -r" to get it back.

You can also have multiple "windows" within the screen. You should find a tutorial if you want to learn more. I typically use screen when coding, I have the code on one and I compile/test it on the other.
June 16, 2007, 6:48 PM
St0rm.iD
You mean your server quits when you exit ssh? Try "man nohup"

You code looks good (I'm not going to compile and test it, but I'll trust you if you say it works!).

At this point, I'd say your next step is to break it down into several smaller functions (or put it all in a class). If you go the class route, replace initalize() with a constructor. If you go the functions route (plain old C), you are going to ned to pass a struct to each of these functions containing all of the variables you defined at the top of main() in your pastebin. You're going to want to have functions that have names such as:

initialize(localhost, localport) - bind the socket and listen
tick() - perform one iteration of the I/O loop. takes an argument whether or not select() should block
run() - continuously call tick() with blocking set to TRUE
on_ready_accept(socket) - called from tick() when you are ready to accept a connection. Inside here you should actually call accept() and add it to your fd_set. Do all error checking in here as well
on_ready_read(socket) - called when a socket is ready to read. Again, do any actual reading in here
on_ready_write(socket) - called when a socket is ready to write. You'll need to modify your code to support this.
on_socket_except(socket) - called when a socket has an exception. You'll need to modify your code to support this, too.
terminate() - stop the run() loop and clean up the socket

This module or class should be called something that gets across the message "I'm the only thing that does I/O in this whole program"
June 16, 2007, 7:23 PM
rabbit
Okay, mostly done: http://pastebin.com/930882

I'm not sure what you were expecting for on_socket_exception(), so I basically left exceptions to perror().  I'm also not sure what to do with BOOL block in tick()...
June 17, 2007, 8:09 PM
St0rm.iD
Some thoughts:
- I would make memzero a function, not a macro. It'll be easier to debug and is otherwise just not natural.
- I would call _settings something else. Perhaps APP, or SERVER. Conceptually, the struct stores things like the socket reference, which aren't actually settings. This is a nitpick, though...it's not a big deal.

Your code is quite well written though.

In your on_socket_exception(), you should remove the socket from the FD_SET (make sure it's properly cleaned up), and be prepared to deal with conditions such as removing it from the chatroom (when you get to this stage). I like treating READ_SOCKET_CLOSE like a socket exception; copy the code from READ_SOCKET_CLOSE to on_socket_exception(), and call on_socket_exception() in READ_SOCKET_CLOSE.

As far as your block argument goes, it's easy. Look at the man page for select(). If block is true, you want to sit in tick() until an I/O event occurs. You want this in a single-threaded environment where your server is only acting upon I/O operations and not something else (scheduled tasks etc). Often, however, you want to instantly return if there is no work to be done. This occurs in systems where you cannot afford to block the thread. The man page says "if timeout is a nil pointer, the select blocks indefinitely." Pass NULL when block is true. The man page also says "to effect a poll, the timeout argument should be non-nil, pointing to a zero-valued timeval structure." Checking if an I/O event has occurred, but not waiting for it is called polling. Do this when block is false.

The next step is to prepare the event system. You are tasked with the following:
- Create a structure which represents an event. You are going to have three types of events: reads, writes, and errors. You're going to run into a problem: conceptually, these are three different events, and should go into three different structures. With C++, we would use inheritance, but in C, we're going to use something called a void pointer. These are pointers that can point to whatever type of data you want, but be careful: they are VERY prone to bugs if you aren't careful. You are also going to be using something called an enumeration (enum), which is essentially an easy way to bind names to serial numbers (i.e. A to 1, B to 2, etc). So, your event structure will have two fields: type, which is an enum of (read, write, exception), and a void pointer (void*) to the actual event structure (you'll need three structures, one for each event type. They should contain all of the data pertaining to each event (socket handles, data buffers if needed etc). For the data buffer, document the fact that the consumer of the event must clean up the memory for the data buffer.
- Create two functions, queue_get(queue, *event) and queue_put(queue, *event). These are FIFO operations (first in, first out). You'll also need to create the datastructure for the queue. It doesn't matter if this implementation is good or not. We're in a phase of development called prototyping. Eventually, we're going to use a multithreaded queue provided to us by Linux, but for now, we are using a placeholder. For simplicity's sake, I'd suggest a struct containing a constant size preallocated array of pointers to event objects, and 2 integers specifying the current indices of the start of the queue and the end of the queue. Also, stick in the documentation of queue_get that the event should probably be allocated on the heap, and in the documentation of queue_put, that the caller should probably be responsible for freeing the memory of the event. This is complicated stuff, feel free to ask me a question about it. The queue implementation, though a good exercise, is something we're just going to throw out anyway, so if it takes you too long, ask me and I'll write a quick implementation (good brush up for me too).

Also, feel free to start reading up on pthreads.

Good luck.
June 18, 2007, 2:57 AM

Search