2020-11-21Computer
Network: Socket Programming
Download experiment
code here
1. Experiment
Objectives
Master the main characteristics and working
principles of TCP and UDP protocols
Understand the basic concepts and working
principles of sockets
Implement socket network communication through
programming (C/Python/Java)
2. Experiment
Code Implementation
2.1 Code
Organization Structure
The code for the 3 projects (6 programs) implemented
in this experiment are as follows:
server_reg.c and
client_reg.c: Simple network registration
server and client
server_check.c and
client_check.c: Simple network check-in
server and client
server_chat.c and
client_chat.c: UDP chat room server and
client
These codes depend on the following self-written
function libraries:
csutil.c: Client & Server
Utility Function, containing functions required for
communication between clients and servers, encapsulating
many socket operations
stuutil.c: Student Utility Function,
encapsulating functions for creating, querying, and
writing to student databases
chatutil.c: Chatting Utility
Function, implementing a custom chat application layer
protocol based on UDP
It also includes the header files for these three
function libraries: csutil.h,
stuutil.h, and chatutil.h.
To facilitate compiling and linking the numerous code
files together, a Makefile was also written. The 6
programs of this experiment are compiled and output as
server_reg, client_reg,
server_check, client_check,
server_chat, and client_chat
respectively.
2.2
Socket Operation Encapsulation in
csutil.c
Since this experiment focuses on socket programming,
regular operations can be written into functions in
csutil.c. This way, when duplicate
functions are needed later, simply adding
#include "csutil.h" allows reuse of this
code.
2.2.1 Exception
Handling Function
Many functions when using sockets may generate
errors. According to UNIX programming principles, when
an exception occurs in a function provided by UNIX, the
exception code is stored in the extern
global variable errno, which requires
including the errno.h header file. To
obtain the string information corresponding to the error
code, the perror function can be used,
which requires including the string.h
header file.
Here, due to the simplicity of the program logic,
once an error is encountered, the function name where
the error occurred and the error message are printed,
and the program exits with a return value of 1:
// Exception handling. If an exception occurs, print error information and exit directly
// funcname: Name of the function where the error occurred
void throw_exception(char *funcname)
{
perror(funcname);
exit(1);
}
2.2.2
Conversion Functions between IP:port and
sockaddr Structure
When using UNIX’s bind,
connect, and sendto functions,
a sockaddr structure needs to be passed in.
However, we usually manually enter an IP address and
port number. Therefore, the get_addr
function was written to convert an IP+port number to a
sockaddr_in structure:
// Convert IPv4:Port to `struct sockaddr_in`
// ip: IP address to be converted
// port: Port number to be converted
// addr: Pointer to the structure for storing results
void get_addr(char *ip, int port, struct sockaddr_in *addr)
{
int ip_net, port_net;
if (inet_pton(AF_INET, ip, &ip_net) < 0)
throw_exception("inet_pton");
port_net = htons(port);
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = ip_net;
addr->sin_port = port_net;
bzero(addr->sin_zero, sizeof(addr->sin_zero));
}
Meanwhile, when using UNIX’s accept and
recvfrom functions, we need to know which
client (from which IP and port) sent data to us.
Therefore, it is also necessary to convert the
sockaddr structure obtained from these two
functions to an IP+port number:
// Convert `struct sockaddr_in` to IPv4:Port
// ip: Pointer to store IP address
// port: Pointer to store port number
// addr: Pointer to the structure to be converted
void get_ip_port(char *ip, int *port, struct sockaddr_in *addr)
{
if (inet_ntop(AF_INET, &addr->sin_addr.s_addr, ip, INET_ADDRSTRLEN) == NULL)
throw_exception("inet_ntop");
*port = ntohs(addr->sin_port);
}
The conversion functions here do not use
inet_addr, inet_ntoa, and
other functions provided in the experiment PPT, because
Chapter 16 of the book “Advanced Programming in the UNIX
Environment” mentions that these two functions have
poorer compatibility compared to inet_pton
and inet_ntop used here.
These conversions only consider IPv4 address strings
and do not support inputting domain names for IP address
resolution. For IPv6 addresses, replace the
sockaddr_in structure with
sockaddr_in6 and AF_INET with
AF_INET6.
2.2.2 Server Creation
Function
2.2.2.1 Function
Prototype
Since the code for creating a server is frequently
reused in this experiment, the process of creating a
server is encapsulated into the make_server
function. The function prototype is as follows:
// Create a server running on `sip`:`sport`
// sip: Server IP
// sport: Server port
// protocol: Application layer protocol, can be TCP or UDP
// handler: Function to handle data sending and receiving
// block: Whether to block connections (whether it is iterative mode)
void make_server(char *sip, int sport, int protocol, void (*handler)(int), bool block);
sip and sport: This
function runs the server on sip:sport. In
variable names appearing below, those prefixed with
s indicate server-side information, and
those prefixed with c indicate client-side
information.
protocol: The transport layer
protocol of the server is specified by
protocol, which can be TCP or
UDP. These macro constants are defined in
csutil.h as SOCK_STREAM and
SOCK_DGRAM respectively.
handler: The processing function
when the server encounters a user connection. This
function needs to accept one parameter, which is the
(socket) file descriptor fd used for the
connection, i.e., the return value of the
accept function (for TCP protocol) or the
return value of the socket function (for
UDP protocol).
block: Whether the server blocks
during the process of handling user connections. That
is, whether the server can handle concurrent requests
simultaneously with multiple processes.
The block parameter only applies to TCP.
For UDP chat rooms, since UDP is connectionless, there
will be no blocking when connecting with clients, so
this parameter can be set arbitrarily.
The specific implementation of this function is as
follows. First, perform the fixed socket
and bind operations:
char cip[INET_ADDRSTRLEN];
int cport, sfd, cfd, caddr_len;
struct sockaddr_in saddr, caddr;
pid_t pid;
if ((sfd = socket(AF_INET, protocol, 0)) < 0)
throw_exception("socket");
get_addr(sip, sport, &saddr);
if (bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
throw_exception("bind");
printf("Serving at %s:%d\n", sip, sport);
Here, the length of the cip array used
to store the client IP address is set to the maximum
length of an IPv4 address INET_ADDRSTRLEN,
which is 16 (including the final \0). This
constant is defined in the arpa/inet.h
header file.
2.2.2.2 Iterative TCP
Server
Next, perform different operations according to
different options. First is the iterative TCP
server:
// Iterative TCP
if (protocol == TCP && block)
{
if (listen(sfd, SOMAXCONN) < 0)
throw_exception("listen");
while (1)
{
if ((cfd = accept(sfd, (struct sockaddr *)&caddr, &caddr_len)) < 0)
throw_exception("accept");
get_ip_port(ip, &port, &caddr);
printf("\nAccepted connection from %s:%d\n", ip, port);
handler(cfd);
close(cfd);
}
}
It is necessary to first call listen,
then simply handle one accept in a loop.
After accept, the client’s address
information is printed in a user-friendly manner, and
then the handler function is called to
implement custom socket read/write operations to
communicate with the client.
The second parameter of listen here is
not set to 0 as in the experiment PPT. According to UNIX
documentation, the maximum number of TCP connections is
defined by the macro SOMAXCONN in the
sys/socket.h header file. On the local
Linux system, this value is 128.
2.2.2.3 Concurrent TCP
Server
Second is the concurrent TCP server:
// Concurrent TCP
else if (protocol == TCP && !block)
{
if (listen(sfd, SOMAXCONN) < 0)
throw_exception("listen");
signal(SIGCHLD, SIG_IGN); // Avoid zombie processes
while (1)
{
if ((cfd = accept(sfd, (struct sockaddr *)&caddr, &caddr_len)) < 0)
throw_exception("accept");
get_ip_port(ip, &port, &caddr);
printf("\nAccepted connection from %s:%d\n", ip, port);
if ((pid = fork()) < 0)
throw_exception("fork");
else if (pid == 0)
{
close(sfd);
handler(cfd);
close(cfd);
exit(0);
}
close(cfd);
}
}
Unlike the iterative TCP server, immediately after
accept, the program calls fork
to create a child process, which handles the newly
established connection, while the parent process
continues to loop and wait for other connections.
Note some details. In UNIX systems, if the parent
process does not call a series of functions in
sys.wait.h to obtain the child process’s
termination information before the child process ends,
the child process becomes a zombie process, occupying
system resources. However, we cannot use
wait in the while loop of the
parent process here, because once wait is
used in the loop, our parent process is also blocked,
becoming an iterative TCP server. Therefore, there are
two ways to avoid this:
One is to fork twice, handle the
request in the child process of the child process, end
the child process in advance, and use wait
to obtain the child process information. This way, the
child process of the child process will be taken over by
the init process, avoiding the generation
of zombie processes.
The other is through a signal handling function.
When a child process terminates, the parent process
receives the SIGCHLD signal. Therefore, if
we perform wait when receiving this signal,
or directly ignore the signal and let the child process
information be deleted directly from the system’s
process table entry, zombie processes can be avoided.
Here, the same approach as in the experiment PPT is used
to ignore the signal: call
signal(SIGCHLD, SIG_IGN) in the parent
process, which requires including the
signal.h header file.
At the same time, to further save resources, when a
process calls fork, its file table entry is
also copied, just like calling the dup
function. However, in the child process here, we do not
need the parent process’s sfd (socket file
descriptor), and in the parent process, we do not need
the child process’s cfd (file descriptor
for a TCP connection). Therefore, they can be closed in
their respective branches.
2.2.2.4 UDP Server
Since UDP is connectionless, its implementation is
the simplest, requiring neither listen nor
accept, and directly calling
handler in a loop for data sending and
receiving:
// UDP
else if (protocol == UDP)
{
while (1)
handler(sfd);
}
2.2.3
Function for Clients to Initiate Communication with
Servers
The client initiating communication with the server
is also a frequently reused function in this experiment.
Similar to creating a server, this function needs to
provide parameters such as server IP, server port
number, transport layer protocol, and
handler function. Then the function
establishes a connection with the server through
connect, obtains the file descriptor
cfd used for the connection, then calls
handler, passing in the cfd
parameter for data sending and receiving processing. The
specific implementation is as follows:
// Initiate a connection to the server at `sip`:`sport`
// sip: Server IP
// sport: Server port
// protocol: Application layer protocol, can be TCP or UDP
// handler: Function to handle data sending and receiving
void contact_server(char *sip, int sport, int protocol, void (*handler)(int))
{
int fd;
struct sockaddr_in saddr;
if ((fd = socket(AF_INET, protocol, 0)) < 0)
throw_exception("socket");
get_addr(sip, sport, &saddr);
if (connect(fd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
// UDP works too
throw_exception("connect");
handler(fd);
close(fd);
}
Note that connect here does not exclude
the UDP protocol! This is because according to Section
16.4 of Chapter 16 in “Advanced Programming in the UNIX
Environment”, the connect function can also
be called under the UDP protocol. The effect of doing
this is that each time send is used to send
data, it will be sent to the address specified by the
connect function by default. Moreover, when
calling the recv function, it will only
receive datagrams from the address specified by the
connect function. There is no need for us
to specify the address each time using the
sendto and recvfrom
functions.
2.2.4 Data
Sending and Receiving Functions
Since the sending and receiving functions provided by
UNIX are still relatively low-level and lack exception
handling, more user-friendly encapsulations have been
written for the recv, send,
recvfrom, and sendto
functions: recv_data,
send_data, recv_data_from,
send_data_to, with exception handling
added. Taking recv_data_from as an
example:
// Encapsulated and more convenient `recvfrom`
// fd: socket file descriptor
// buf: buffer
// ip: pointer to store the sender's IP
// port: pointer to store the sender's port
void recv_data_from(int fd, void *buf, char *ip, int *port)
{
struct sockaddr_in caddr;
int caddr_len = sizeof(caddr);
bzero(buf, MAXLINE);
again:
if (recvfrom(fd, buf, MAXLINE, 0, (struct sockaddr *)&caddr, &caddr_len) < 0)
{
if (errno == EINTR)
goto again; // Prevent interrupted system call
throw_exception("recvfrom");
}
get_ip_port(ip, port, &caddr);
}
It can be seen that it does not need to pass in a
sockaddr structure, but directly passes in
the addresses to receive the IP and port number. And
there is no need to specify the buffer length, because
in csutil.h, the length of the buffer for
both the sender and receiver is uniformly set to
MAXLINE, which is 8192. There is also no
need to manually initialize the buffer to 0 before each
reception, because the function has already done
zero-filling for us through bzero.
Note that goto is used in this function
to prevent recv series functions from being
interrupted during execution. Because signal handling
functions are used in subsequent programs, when the
program receives a signal, for ordinary system calls,
they will be executed to completion before the signal
handling function is executed, but for slow system
calls, such as reading and writing on network
sockets, when a signal is received, the system
call will be interrupted and return the error code
EINTR. Therefore, if an error is reported
and exited directly at this time, the required functions
of the program cannot be achieved. The solution is to
re-execute the current system call once this error is
encountered, and the simplest solution is to use
goto.
This is also the sample approach in Chapter 10 of
“Advanced Programming in the UNIX Environment”.
Similarly, taking the send_data function
as an example:
// Encapsulated and more convenient `send`
// fd: socket file descriptor
// buf: buffer
// len: length of the data part to be sent, automatically calculated if 0
void send_data(int fd, void *buf, int len)
{
if (len == 0)
len = strlen(buf) + 1;
if (send(fd, buf, len, 0) < 0)
throw_exception("send");
}
It can be seen that it provides an option: if the
length is set to 0, there is no need for us to manually
calculate the length of the data to be sent. This is
very user-friendly when sending string constants.
2.3
Simple Network Registration Service (Iterative)
2.3.1
stuutil.c Student Database
To implement student information storage, addition,
and query, stuutil.c was written. Here,
student information is stored in the form of a text
file: the first line is the student’s name, and the
second line is the student’s ID number. Each student’s
information is also separated by a line. The location of
the text file is DB_PATH defined in
stuutil.h, which is
student.txt in the running directory of the
server program.
The add_student function is needed here
to add student information to the database, that is, to
append information directly to the text file:
// Add a student to the database
// name: student name
// number: student ID
void add_student(char *name, char *number)
{
FILE *fp;
fp = fopen(DB_PATH, "at");
fputs(name, fp);
fputs("\n", fp);
fputs(number, fp);
fputs("\n", fp);
fclose(fp);
}
2.3.2 Server Program
With stuutil.h and the previous
csutil.h, the subsequent coding process is
extremely easy. Because we only need to obtain the IP
and port from the command line, specify the protocol,
and write our own handler function. For
example, the complete code of the server is as
follows:
/* Registration Server */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "csutil.h"
#include "stuutil.h"
// Handle data sending and receiving with the client
// cfd: socket file descriptor
void handler(int cfd)
{
char buf_name[MAXLINE], buf_number[MAXLINE];
while (1)
{
// First round of sending and receiving
send_data(cfd, "Please input student name:", 0);
recv_data(cfd, buf_name);
if (strcmp(buf_name, "bye") == 0)
return;
printf("Student name:\t%s\n", buf_name);
// Second round of sending and receiving
send_data(cfd, "Please input student number:", 0);
recv_data(cfd, buf_number);
printf("Student number:\t%s\n", buf_number);
add_student(buf_name, buf_number);
}
}
int main(int argc, char *argv[])
{
char *ip;
int port;
if (argc != 3)
{
printf("Usage: ./server_reg server_ip server_port\n");
return 0;
}
ip = argv[1];
port = atoi(argv[2]);
make_server(ip, port, TCP, handler, true); // Create an iterative TCP server
return 0;
}
Among them, in the make_server function,
the protocol is set to TCP and the iterative mode is
set. In the handler function that processes
client requests, two rounds of sending and receiving are
performed in each loop, corresponding to the name and
student ID respectively. Exit when the entered name is
bye. At the same time, the received name
and student ID are printed on the server’s screen, and
then stored through the add_student
function mentioned in 2.3.1.
2.3.3 Client Program
For the client, its structure is roughly similar.
Only need to replace make_server in the
main function with
contact_server mentioned in 2.2.3, and
implement the handler to handle the
connection with the server. The implementation of the
client’s handler function is as
follows:
// Handle data sending and receiving with the server
// cfd: socket file descriptor
void handler(int cfd)
{
char buf[MAXLINE];
while (1)
{
// First round of sending and receiving, input name
recv_data(cfd, buf);
printf("[Server] %s\n[Client] ", buf);
fgets(buf, MAXLINE, stdin);
buf[strlen(buf) - 1] = '\0';
send_data(cfd, buf, 0);
if (strcmp(buf, "bye") == 0)
break;
// Second round of sending and receiving, input student ID
recv_data(cfd, buf);
printf("[Server] %s\n[Client] ", buf);
fgets(buf, MAXLINE, stdin);
buf[strlen(buf) - 1] = '\0';
send_data(cfd, buf, 0);
}
}
Two rounds of sending and receiving are also
performed. Exit if the input is bye.
2.4
Simple Network Check-in Service (Concurrent)
2.4.1
stuutil.c Student Database
Same as Section 2.3.1, implement a simple student
query function has_student in
stuutil.c:
// Check if the student exists in the database
// name: student name
// return: true if exists, false if not
bool has_student(char *name)
{
char buf[MAXLINE];
int len;
FILE *fp;
fp = fopen(DB_PATH, "rt");
while (!feof(fp))
{
fgets(buf, MAXLINE, fp);
buf[strlen(buf) - 1] = '\0';
if (strcmp(buf, name) == 0)
{
fclose(fp);
return true;
}
fgets(buf, MAXLINE, fp);
buf[strlen(buf) - 1] = '\0';
}
fclose(fp);
return false;
}
Since check-in only considers the name, if the
student’s name exists, return true.
2.4.2 Server Program
The main function of the server program only needs to
change the last parameter in make_server
from true in 2.3.2 to false,
that is, create a concurrent TCP server:
make_server(ip, port, TCP, handler, false);
The handler function of the server
program is as follows:
// Handle data sending and receiving with the client
// cfd: socket file descriptor
void handler(int cfd)
{
char buf[MAXLINE];
send_data(cfd, "Please input student name:", 0);
while (1)
{
recv_data(cfd, buf);
if (strcmp(buf, "bye") == 0) // Case of "bye"
return;
if (has_student(buf)) // Case of finding the student (exists)
{
send_data(cfd, "Successfully checked in! Next name:", 0);
printf("%s checked in\n", buf);
}
else // Case of finding the student (does not exist)
send_data(cfd, "No such student! Next name:", 0);
}
}
Only one round of sending and receiving is performed
per loop. If the received student name exists, it is
also output on the server’s screen (convenient for
teachers to view check-in status in the background). If
it does not exist, an error message is also sent to the
client. If bye is received, exit.
2.4.3 Client Program
The client’s handler is simpler,
performing one round of sending and receiving per loop,
and exiting if bye is input:
// Handle data sending and receiving with the server
// cfd: socket file descriptor
void handler(int cfd)
{
char buf[MAXLINE];
while (1)
{
// Receive server prompt
recv_data(cfd, buf);
printf("[Server] %s\n[Client] ", buf);
// Send name to server
fgets(buf, MAXLINE, stdin);
buf[strlen(buf) - 1] = '\0';
send_data(cfd, buf, 0);
if (strcmp(buf, "bye") == 0) // Exit loop if "bye" is input
break;
}
}
2.5 Chat Room
Based on UDP Socket
2.5.1
chatutil.c Application Layer Protocol
To implement chat room user registration, message
transmission from clients to servers, and server
broadcasting of messages based on the UDP protocol, the
packet format of a self-designed application layer
protocol (hereinafter referred to as the Simple Chat
Protocol) is defined in chatutil.h.
The header part structure is
struct header:
#pragma once
#pragma pack(push, 1)
struct header // Application layer packet header
{
uint16_t id;
uint8_t flags;
uint8_t len;
};
#pragma pack(pop)
#pragma pack(1) is used to enforce
1-byte alignment. It corresponds to the following:
| Client ID (high 8 bits) | Client ID (low 8 bits) |
| Flag bits | Data part length |
The data part immediately follows the header.
Client ID: Each client connected to the UDP chat
room server has a unique client ID to identify the
client’s identity. If a client has just entered the chat
room but not yet registered, the default ID (value 0) is
used. Since the ID is 16 bits, this UDP server can
support a maximum of 65535 clients chatting
simultaneously.
Flag bits: Used to mark the function of the
packet. The specific distribution of the 8 bits from
high to low is:
| Reserved | Reserved | BRD | FIN | SND | REG | NAK | ACK |
ACK: Confirmation packet from the server agreeing
to registration, and confirmation packet responding to
messages sent by the client
NAK: Negative packet from the server refusing
registration (duplicate name, maximum number of online
clients reached)
REG: Registration request packet from the
client
SND: Chat message packet sent by the client to
the server
FIN: Packet from the client exiting the chat
room
BRD: Packet from the server broadcasting chat
messages and other notification information to
clients
Data part length: Length of the data part in
bytes.
2.5.1.2
Protocol Confirmation Mechanism
To ensure that the client can detect in a timely
manner if the server has crashed (instead of
continuously receiving nothing and not knowing whether
no one is sending messages or the server has crashed), a
confirmation mechanism is introduced for communication
from the client to the server:
The server will send a REG ACK or REG NAK
confirmation in response to the REG packet sent by the
client
The server will send an SND ACK confirmation in
response to the SND packet sent by the client
The server will send a FIN ACK confirmation in
response to the FIN packet sent by the client
If the client does not receive confirmation
within a certain period of time, it is considered that
the server has crashed. The timeout period is defined as
TIMEOUT in chatutil.h, which
is 8 seconds.
2.5.1.2
Application Layer Sending and Receiving Functions
To handle the self-designed application layer
protocol, sending and receiving functions for this
protocol also need to be designed. The sending and
receiving of the server and client are different. Due to
the connectionless nature of UDP, the server needs to
know the address of the other party, i.e.,
sendto and recvfrom must be
used. Taking the server’s sending function as an
example:
// Server sends Simple Chat Protocol packets
// cfd: socket file descriptor
// cip: recipient client IP address
// cport: recipient client port number
// id: recipient client ID
// flags: protocol packet flag bits
// data: protocol packet data part
void server_send_chat(int cfd, char *cip, int cport, int id, int flags, char *data)
{
int len = strlen(data) + 1;
struct header hd;
char buf[sizeof(hd) + len];
hd.id = id;
hd.flags = flags;
hd.len = len;
memcpy(buf, &hd, sizeof(hd));
memcpy(buf + sizeof(hd), data, len);
send_data_to(cfd, buf, cip, cport, sizeof(hd) + len);
}
It is necessary to know the ID, IP address, and port
number of the client to send to, as well as the flags to
specify. Then the function first constructs the header
of the Simple Chat Protocol using this information, then
splices the header and data part together, copies them
into buf, and then sends them using the
send_data_to function in
csutil.c.
Taking the client’s receiving function as another
example. Since in 2.2.3, the client has specified the
server’s address in advance through the
connect function, the client does not need
to specify the server’s address again in the sending and
receiving functions:
// Client receives Simple Chat Protocol packets
// cfd: socket file descriptor
// flags: pointer to protocol packet flag bits
// data: pointer to protocol packet data part
// timeout: time to wait for ACK (seconds), server is considered down if exceeded
// return: sender client ID
int client_recv_chat(int cfd, int *flags, char *data, int timeout)
{
struct timeval tv;
struct header hd;
char buf[MAXLINE];
tv.tv_usec = 0;
tv.tv_sec = timeout;
if (setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) // Set receive wait time
throw_exception("setsockopt");
recv_data(cfd, buf);
tv.tv_sec = 0;
if (setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) // Restore receive wait time
throw_exception("setsockopt");
memcpy(&hd, buf, sizeof(hd));
memcpy(data, buf + sizeof(hd), hd.len);
*flags = (int)hd.flags;
return hd.id;
}
Similarly, after the client receives the data, it
first stores it in buf, then copies it to
the header structure and data
data part in sequence. Then assign the
flags field in header to the
variable pointed to by the flags pointer,
and return the client ID in header.
To account for the situation where the server
suddenly goes offline, this function accepts a
timeout parameter (in seconds). The timeout
time for recv series functions is set
through the setsockopt function. If a
timeout occurs, recv will generate an error
message of “Resource not available” and exit the
program.
2.5.2 Server Program
2.5.2.1 Main Function
Part
Similar to the server programs in 2.3.2 and 2.4.2,
only need to change the protocol parameter of
make_server to UDP in the main
function:
make_server(ip, port, UDP, handler, false);
Define a client structure to store
individual user information, including IP, port number,
name, and whether alive:
// Client information structure
struct client
{
bool alive; // Whether alive
char ip[INET_ADDRSTRLEN]; // Client IP
char name[MAXNAME]; // Client name
int port; // Client port number
};
The mechanism for introducing the alive
field here is: during initialization, all
alive values are false. When a
client connects, the alive corresponding to
the client ID is set to true. When the
client sends a FIN to the server, the server sets the
alive corresponding to the client ID to
false. When a new user joins, if it is
found that the content corresponding to an ID is
false, the ID can be assigned to the new
user, directly overwriting the original data. This
eliminates the need for adding and deleting array
elements.
Then, create a clis array where the
array subscript is the client ID. This allows obtaining
client information with a single lookup by ID, without
writing a loop for searching:
struct client clis[MAXCLIENT];
To check for duplicate names during user
registration, the has_name function is
written:
// Check if the name exists among currently connected clients
// name: name to find
// return: true if exists, false if not
bool has_name(char *name)
{
for (int i = 1; i < MAXCLIENT; i++)
if (clis[i].alive && strcmp(clis[i].name, name) == 0)
return true;
return false;
}
After agreeing to user registration, an ID needs to
be assigned to the user, and the user information needs
to be stored in the structure corresponding to the ID.
Therefore, the cli_alloc function is
written:
// Allocate an ID for a newly joined client
// name: client name
// ip: client IP address
// port: client port number
// return: assigned client ID (0 means full)
int cli_alloc(char *name, char *ip, int port)
{
for (int i = 1; i < MAXCLIENT; i++)
if (!clis[i].alive)
{
clis[i].alive = true;
strcpy(clis[i].name, name);
strcpy(clis[i].ip, ip);
clis[i].port = port;
return i;
}
return 0;
}
Detect the first client with the alive
field set to false starting from subscript
1 and assign it to the new client. If the server is
full, return 0.
2.5.2.3
Application Layer Packet Processing
In the application layer handler
function, first call server_recv_chat in
chatutil.c to receive an application layer
packet of the Simple Chat Protocol sent by a client, and
print the packet information:
char cip[INET_ADDRSTRLEN], data[MAXDATALEN], appended_data[MAXDATALEN]; // appended_data is the modified sending content by the server
int cport, id, flags;
// Print information of the application layer packet received by the server
id = server_recv_chat(sfd, cip, &cport, &flags, data);
printf("%s:%d, id=%d, flags=%d, data=%s\n", cip, cport, id, flags, data);
Then, perform different processing according to the
different flag bits of the packet. First is the REG flag
bit, handling user registration:
// Case of receiving registration packet
if (flags & F_REG)
{
if (has_name(data)) // Name conflict, send NAK packet feedback
server_send_chat(sfd, cip, cport, id, F_REG | F_NAK, "Name has already used");
else
{
id = cli_alloc(data, cip, cport);
if (id != 0)
{
server_send_chat(sfd, cip, cport, id, F_REG | F_ACK, ""); // Registration successful, send ACK packet feedback
sprintf(appended_data, "%s (%s:%d) joins the conversation", clis[id].name, clis[id].ip, clis[id].port);
for (int i = 1; i < MAXCLIENT; i++) // Broadcast to other clients that this member has joined
if (clis[i].alive)
server_send_chat(sfd, clis[i].ip, clis[i].port, i, F_BRD, appended_data);
}
else // Server full, send NAK packet feedback
server_send_chat(sfd, cip, cport, id, F_REG | F_NAK, "Server is full");
}
}
When user registration is successful, the server
broadcasts the information that the client has joined
the chat to all clients (including the registered
client).
Next, consider the SND flag bit:
// Case of receiving send packet
else if (flags & F_SND)
{
server_send_chat(sfd, cip, cport, id, F_SND | F_ACK, ""); // Send ACK packet feedback
sprintf(appended_data, "[%s] %s", clis[id].name, data);
for (int i = 1; i < MAXCLIENT; i++) // Broadcast the message sent by this client to other clients
if (clis[i].alive && i != id)
server_send_chat(sfd, clis[i].ip, clis[i].port, i, F_BRD, appended_data);
}
The server first replies with an ACK to the sender,
then forwards the chat message received from the client
to other clients.
Finally, consider the FIN flag bit:
// Case of receiving finish packet
else if (flags & F_FIN)
{
server_send_chat(sfd, cip, cport, id, F_FIN | F_ACK, ""); // Send ACK packet feedback
clis[id].alive = false;
sprintf(appended_data, "%s (%s:%d) leaves the conversation", clis[id].name, clis[id].ip, clis[id].port);
for (int i = 1; i < MAXCLIENT; i++) // Broadcast to other clients that this member has left
if (clis[i].alive && i != id)
server_send_chat(sfd, clis[i].ip, clis[i].port, i, F_BRD, appended_data);
}
The server also first replies with an ACK, then sets
the client’s flag bit to false, and finally
broadcasts the information that the member has left to
other members.
2.5.3 Client Program
2.5.3.1 Main Function
Part
Because ordinary program input and output can only be
performed sequentially, just like the previous
registration and check-in services, each performing one
round of sending and receiving. However, chat room users
should be able to send messages whenever they want.
Therefore, the main function first registers the
processing function switch_mode for the
Ctrl+C signal, allowing the client to immediately switch
to input mode after pressing Ctrl+C.
The part for connecting to the server only needs to
slightly modify the parameters of
contact_server to UDP:
signal(SIGINT, switch_mode); // Register the handler for the Ctrl+C signal. During chatting, Ctrl+C switches to input mode
...
contact_server(ip, port, UDP, handler);
The subsequent registration and message receiving
process are implemented by handler, while
the message sending process is implemented by signal
handling.
2.5.3.2
Registration and Message Receiving Process
First is the registration process:
// Registration process
while (1)
{
printf("Please input your name: ");
fgets(name, MAXNAME, stdin);
name[strlen(name) - 1] = '\0';
client_send_chat(cfd, 0, F_REG, name);
while (1) // Wait for F_ACK from the server, exit directly with error if timeout
{
id = client_recv_chat(cfd, &flags, data, TIMEOUT);
if ((flags & F_REG) && (flags & (F_ACK | F_NAK)))
break;
}
if (flags & F_ACK) // If the server agrees to registration, exit the loop; otherwise, choose a new name
break;
printf("%s\n", data);
}
It can be seen that a timeout mechanism is also
introduced in the registration process. If no ACK or NAK
reply is received within the timeout period, the server
is considered down and the program exits.
The message receiving process is simple: just keep
looping to receive and print:
// Message receiving process
while (1)
{
client_recv_chat(cfd, &flags, data, 0);
if (flags & F_BRD)
printf("%s\n", data);
}
Note that the timeout period here is set to 0
(infinite), because there may be situations where
everyone is idle and not speaking. Therefore, to detect
if the server is functioning properly, it is necessary
to rely on the confirmation in the subsequent message
sending process.
2.5.3.4 Message Sending
Process
The following is part of the code for
switch_mode:
// Switch to message input mode (triggered by pressing Ctrl+C)
void switch_mode()
{
...
// Print own name, prompt for input, get input
printf("\r[%s] ", name);
fgets(data, MAXDATALEN, stdin);
data[strlen(data) - 1] = '\0';
...
// Case of inputting other chat information
client_send_chat(cfd, id, F_SND, data);
while (1) // Wait for F_ACK from the server, exit directly with error if timeout
{
client_recv_chat(cfd, &flags, data, TIMEOUT);
if ((flags & F_SND) && (flags & F_ACK))
break;
else if (flags & F_BRD)
printf("%s\n", data);
}
}
It can be seen that after pressing Ctrl+C, the screen
first prints the user’s own name to prompt for input,
then after pressing Enter, the client sends the input
information to the server and starts a timeout to wait
for the server’s ACK packet. If the packet is not
received within the timeout period, the server is
considered down and the program ends. If a BRD broadcast
message is received before the ACK, the message is also
printed normally.
3. Code Running
and Exploration
Use the make command to perform
automated compilation with the previously written
Makefile, then enter ./program_name ip port
to run the client or server.
3.1
Simple Network Registration Service (Iterative)
3.1.1 Normal Running
Effect
Open 1 server (middle right), 4 clients (left side
and top right), and a terminal to view the
netstat command. The effect is as
follows.
Step 1: Start the server, running on local port 6666.
Then start the clients in the order of top left, middle
left, bottom left, top right. Then the top left client
communicates with the server first, and the others wait.
From netstat, all connections are in the
ESTABLISHED state, meaning the three-way
handshake is completed. And from the information output
by the server, the process with port number 36656 is
currently communicating with the server. As shown in the
figure below:

Step 2: Enter bye in the top left
client, then the middle left client immediately starts
communicating with the server. Meanwhile, from
netstat, the original connection from 6666
to 36656 is closed, and the connection from port 36656
to port 6666 is in the TIME_WAIT state
before closing. This is because, as shown in Figure 5-29
of the textbook, after the server receives the client’s
FIN and FIN ACK, it closes the connection, while the
client waits for 2MSL before closing the connection
after receiving the server’s FIN ACK. And from the
server output information, the current client port
number is 36658. As shown in the figure below:

Step 3: Enter bye in the middle left
client, and the bottom left client immediately connects.
It can be seen that connections are processed in the
order of connection initiation. At this time, in
netstat, the connection from port 56658 to
port 6666 is also in the TIME_WAIT state,
while the poor top right client is still waiting. As
shown in the figure below:

Step 4: Enter bye in the bottom left
client, the top right client communicates with the
server, then enter bye after inputting.
After waiting for a period of time (to exceed 2MSL),
check with netstat again, and all the above
connections are released. As shown in the figure
below:

It can be seen that its performance meets
expectations.
As mentioned above, the backlog of TCP
connections here is set to SOMAXCONN, which
is 128 on the local system, so the connections of these
clients are all accepted and kept waiting.
3.1.2 TCP
Connection Limit Scenario
Previously, in the make_server function
of csutil.c, the number of TCP connections
(i.e., the backlog parameter of
listen) was set to SOMAXCONN,
which is 128, neither 0 nor 1. If it is
changed to 0 and the above steps are repeated, as shown
in the figure below:

It can be found that when using netstat
in the bottom right corner, the connections from the
clients with the highest port numbers (36690 for bottom
left, 36692 for top right) to 6666 are in the
SYN_SENT state, completing only one
handshake. And soon after, these two clients show a
“Connection timed out” error, indicating that they did
not successfully establish a connection with the server.
This is because, as shown in Figure 5-28 of the
textbook, a three-way handshake is required before a TCP
connection is established. Due to the limit on the
maximum number of TCP connections, after the last two
clients send a SYN, the server does not respond with a
SYN ACK, resulting in only one handshake being completed
and the connection ending due to handshake timeout.
Now change it to 1 and repeat the above steps, as
shown in the figure below:

It can be seen that the connection capacity for one
more client is added this time. Based on this, it is
speculated that the maximum number of connectable
clients may be backlog + 2.
3.1.3
Client Binding to Fixed Port Scenario
To do this, insert the following code into the
contact_server function of
csutil.c to bind the client to local port
5555:
struct sockaddr_in caddr;
get_addr("127.0.0.1", 5555, &caddr);
if (bind(fd, (struct sockaddr *)&caddr, sizeof(caddr)) < 0)
throw_exception("bind");
Then, recompile with make, first let the
client communicate with the server, then close it, and
then communicate with the server again. The first
communication is normal, but if communication is
attempted immediately after ending, the client program
will prompt “Address already in use”, indicating that
the previously bound port number 5555 is already
occupied, as shown in the figure below:

It can be seen that the previously established
connection is still in the TIME_WAIT state.
Therefore, if binding to the same port number again
before the connection is completely released, binding
will fail because the previous port number is still in
use. However, if a connection is initiated after waiting
for a period of time (after 2MSL), communication can be
resumed:

Therefore, it is not recommended for clients to bind
to the same port number because after the client closes
the connection, the connection needs to wait for a
period of time to be completely released, so the port
number cannot be reused immediately, making it
impossible to establish a connection for a period of
time.
3.1.4
Byte Order Conversion of IP Address and Port
Since the inet_ntop and
inet_pton functions have built-in network
byte order conversion. Therefore, only the scenario
where the network byte order of the port is not
converted can be demonstrated. Change in the
get_addr function in
csutil.c:
port_net = htons(port);
to
```Plain Text
port_net = port; ```
After compiling with make, still run the
server on port 6666:

It can be seen that communication is still possible
normally, because both the client and server use the
get_addr function, so the port named 6666
is actually treated as port 2586. However, in practical
applications, such errors are not allowed.
Next, analyze the reason. Convert 6666 to a 16-bit
binary number, divided into two bytes:
6666_{10}=0001\;1010\quad0000\;1010_2
Reversing the order of these two bytes (while keeping
the order within the bytes unchanged) and converting
back to decimal gives 2586:
0000\;1010\quad0001\,1010_2=2586_{10}
It can be seen that different endianness is used to
store data on the network and on the local machine, so
conversion is necessary.
3.2
Simple Network Check-in Service (Concurrent)
3.2.1 Normal Running
Effect
Open 1 server (bottom right), also running on port
6666; 5 concurrent clients (left side, top right, middle
right). The effect is as follows:
Figure 1: All five clients can establish connections
with the server simultaneously, and the server also
displays information about 5 clients connecting.

Figure 2: The 5 clients can send names to the server
for check-in simultaneously, and the server prints
check-in information. For non-existent names, error
prompts are also provided:

It can be seen that its performance meets
expectations.
3.2.2
Impact of Closing cfd in Parent
Process
In the above code, the descriptor obtained by the
server through socket is sfd,
and the descriptor for a single connection obtained
through accept is cfd. The
above code closes cfd in the parent
process, and some explanations have been mentioned in
2.2.2.3.
The following shows the scenario where the parent
process closes cfd on port 6666 (left side)
and the scenario where the parent process does not close
cfd on port 8888 (right side). There is no
difference in communication, but when the client exits,
the netstat results on both sides are
different:

The left side normally enters the
TIME_WAIT state, and the connection is
completely released soon after. The server on the right
side is in the CLOSE_WAIT state, and the
client is in the FIN_WAIT2 state,
indicating that only two handshakes (FIN and ACK) have
been completed. In other words, the server’s application
process did not close the connection and did not
actively initiate a FIN ACK.
This can be explained by the replication of UNIX file
descriptors. When a process is forked, the
file descriptors in the child process are like being
processed by the dup function, meaning both
the parent and child process file descriptors point to
the same file table, which contains file status, current
offset, and other file information. The file is truly
closed only when the number of file descriptors pointing
to a file table is 0; otherwise, it is only “closed” for
a certain process and removed from the process table of
that process. Therefore, if close is only
called in the child process, although it is removed from
the process table of the child process, it is not
removed from the parent process, so the file table for
the connection still exists, meaning the connection is
not closed. Therefore, it is necessary to close
cfd in the parent process; otherwise,
system resources are wasted and the client cannot
completely release the connection.
3.3 Chat Room
Based on UDP Socket
3.3.1 Running Effect
Also start a server on local port 6666 (bottom
right), then five clients chat. By default, it receives
messages; to send messages, press Ctrl+C:

It can be seen that when registering a name in the
bottom left client, an attempt to use a duplicate name
results in registration failure. And the server sends
prompts about users joining and exiting, along with
their addresses. The server also records logs, retaining
each application layer packet of the Simple Chat
Protocol sent by clients.
It can be seen that during registration (inputting a
name), the id of the packet received by the
server is 0, and flags is 4 (representing
REG). When sending messages, the server receives the
respective user id, and flags
is 8 (representing SND). When a user exits,
flag is 16 (representing FIN).
3.3.2 Exception Capture
Following the above chat. If the server suddenly
crashes (ends by pressing Ctrl+C), the client will
report an error when sending messages to it. Here,
because the port is already closed (rather than the
server not responding), a “Connection refused” error
occurs immediately:

If a public network address is entered randomly, the
packet may never receive a response, and the previously
introduced timeout mechanism takes effect. An error will
be reported and the program will exit after an 8-second
timeout, with the error content being “Resources
unavailable”:

3.3.3
Real Environment: Unreliable Transmission of UDP
Next, deploy the server on port 6666 of the own
server f5soft.site (public network
address: 39.97.114.106:6666), and start 4 clients on the
own computer to connect to the public network server. It
can be found that most functions operate normally.
However, due to the unreliable transmission of UDP and
the unreliability of the underlying layer in the public
network environment (unlike the reliable underlying
layer of the local network), UDP datagram loss
occurred in the public network environment!

It can be seen that UDP is indeed unreliable. To
completely solve this problem, reliable transmission
needs to be implemented in our application layer (Simple
Chat Protocol), so in addition to the previous timeout
mechanism, a retransmission mechanism and other measures
need to be introduced.
4. Experiment Summary
In this experiment, we experienced and mastered:
Reducing program code volume through reasonable
code reuse
Basics of UNIX network socket
programming
UNIX multi-process parallel programming
Acquisition and conversion of socket network
addresses
Creation of TCP and UDP servers
Communication between TCP/UDP clients and
servers
Three-way handshake process for TCP connection
establishment and four-way handshake process for
connection release
Using the netstat command to view
local connections and port status
Implementing a custom application layer protocol
- Simple Chat Protocol
Differences between production and local
environments and verification of UDP’s unreliable
transmission
To compile and run, in a Linux environment, enter
make in the src directory to
generate compiled files
Download experiment
code here