Linux chat app that uses only System V inter-process communication facilities (IPCS) to communicate written in C.
Project created for "System and Concurrent Programming" course at Poznan University of Technology.
The app has two executable files: server and client. It's the client-server architecture, so there's one server and many clients.
Server uses one IPC message queue to communicate with all clients. Users are differentiate by connectionId (msgrcv's msgtyp parameter).
The user defines the IPC queue id during the start of the file.
Structs used during communication are defined as follows:
typedef struct {
long connectionId;
char body[REQUEST_BODY_MAX_SIZE];
unsigned long bodyLength;
RType type;
long responseConnectionId;
int channelId;
} Request;
typedef struct {
long connectionId;
char body[REQUEST_BODY_MAX_SIZE];
unsigned long bodyLength;
RType type;
StatusCode status;
} Response;Purpose of struct's properties:
long connectionId- IPC message type (long msgtyp), treated as userId and the main type for performing requestschar body[]- message body (text)unsigned long bodyLength- length of body (string length)RType type- response/request type defining what server is asked for (during the request) or what contains the responselong responseConnectionId- IPC message type that will be used to send response to requestint channelId- the channel id that user is connected toStatusCode status- response status
RType is an enum defined as follows:
enum eRType {
R_Init = 1,
R_RegisterUser,
R_UnregisterUser,
R_ListChannel,
R_JoinChannel,
R_LeaveChannel,
R_CreateChannel,
R_ListUsersOnChannel,
R_ListUsers,
R_ChannelMessage,
R_PrivateMessage,
R_Heartbeat,
R_EndConnection
};
typedef enum eRType RType;RType property is used in the requests and responses to define what the request/response contains. For example: on the R_ListChannel request, the server will respond with a R_ListChannel response. The response will contain a list of channels in the body.
R_Init and R_RegisterUser are "unregistered requests". It's because during those requests user is not known to the server. During the R_Init request client establishes a connection with the server, and during the R_RegisterUser request client sets its username.
R_EndConnection is used only to send StatusNotVerified to all connected users before shutting down the server.
The rest types are used in general communication to get the data from the server.
StatusCode is an enum defined as follows:
typedef enum {
StatusOK,
StatusServerFull,
StatusValidationError,
StatusNotVerified
} StatusCode;StatusCode is used to define the status of the response from the server. The client can react to the response status without parsing the body.
Usage of statuses:
StatusOK- sent when the request is parsed correctly, and the body has contents that match the request rtypeStatusServerFull- sent when the server has reached the limit of users. Sent only in answer to unregistered requestsStatusValidationError- sent when the request body doesn't match requirements. Example: username is already taken, the username is too long/short.StatusNotVerified- sent when the user that performs request is no longer verified (didn't respond toR_Hearbeat)
During request, every property is filled, but not always all of the properties are used. For example: channelId is used when sending a message to the channel.
Request has a property named responseConnectionId. The server listens to all IPC messages with the type below queueTypeGap (calculated at the start of the server). The responseConnectionId is always above queueTypeGap. That prevents the server from detecting its responses as requests.
During communication with the user, there is one queue with 6 different types (msgtype):
- 2 types are used for general requests. The client asks for the first
connectionIdduringR_Initrequest. By usingconnectionIdclient calculatesresponseConnectionId. - 2 types are used for messages (private and channel). For sending a message the client uses
connectionId + 1. The server sends the message to other clients using their'sresponseConnectionId + 1. - 2 types are used for the heartbeat mechanism. After every request server checks the user's connection. During check, the server sends
R_Heartbeatrequest on user'sconnectionId + 2and listens for answer on user'sresponseConnectionId + 2.
If the response is successful, its body contains information related to the request's RType. Otherwise, the server responds with the error message in body.
Heartbeat is a mechanism that allows the server to verify if the user is still connected to the server. The server listens for the requests in the loop. At the end of the loop, there's a user check.
User check is defined as follows:
- If the last check was performed later than 5 seconds ago, continue
- Send
R_Heartbeatrequest to the client - Wait 10ms for response
- If the client responded, mark as verified. Otherwise, delete the user from the user's list. The next requests are going to be rejected with
StatusNotVerified.
Communication on diagram is described as follows:
Request/Response RType [connectionId]
- RType describes RType used during communication
- connectionId describes name of variable and context of used connectionId (msgtyp)
The server answers with the response of the same RType as the client's request. Only when the user is not verified, the server returns R_NotVerified and refuse to answer the client.
The client listens for channel and private messages on a different process. That makes whole message communication asynchronous.
The server parses the message and adds the additional information: the time of the message, sender, receiver (only when private message), and channel name (only when channel message).
The channel message request contains the message in the body. The channel is specified in the request struct (channelId).
body of private message request contains receiver's username and the message, separated by a space.
gcc -Wall shared/* resources/client/* client.c -o client.out
gcc -Wall shared/* resources/server/* server.c -o server.outExecutable files have help, which can be run by ./<executable-file> --help.
When you run the server file, you'll be asked about the IPC id to create a queue.
You can also provide the queue id by -i <id> or --id <id> parameter.
When you execute the client file, there'll be a question about the IPC id to connect to (it has to exist and be used by the server).
You can also provide that by -s <id> or --server <id> parameter.
After getting the correct server id, there'll be a question about the username. It has to be unique.
You can also provide that by -u <username> or --username <username>.
The server or client files are divided into two categories:
- *-config - that file contains basic config of the app side. For example there you can find
#defines, configuration structures and global variables likeMessages,CommandSignaturesand so on. - *-lib - that file contains all the used functions in the main file.
Both app sides use also shared functionality, that can be found in files:
- shared - the file that has all
#includes to used libraries. - utils - the file that contains basic utility functions, like
printfDebugorgetTimeString. - communication - the file that contains basic protocol's functionality and structs like
Request,Response,sendRequest,sendResponse.