Network programming -- connection oriented server and client programming

reference resources

  1. TCP/IP network programming Yin Shengyu

Preparation of connection oriented server and client

Understanding TCP and UDP

According to different data transmission modes, sockets based on network protocol are generally divided into TCP sockets and UDP sockets. Because TCP socket is connection oriented, it is also called stream socket

TCP is the abbreviation of Transmission Control Protocol, which means "control of data transmission process"

TCP/IP protocol stack

TCP/IP protocol stack is divided into four layers. It can be understood that data sending and receiving is divided into four hierarchical processes. (1) Application layer; (2) TCP/UDP layer; (3) IP layer; (4) Link layer. Each layer may be implemented by software such as operating system or hardware devices similar to NIC

The advantages of dividing the protocol into multiple levels: (1) it is easier to design the protocol; (2) To design open systems through standardized operations

link layer

The link layer is the result of the standardization of the physical link field and is also the most basic field. It specifically defines network standards such as LAN, WAN and MAN

IP layer

In order to transmit data in complex networks, the choice of path needs to be considered first. The IP layer solves the path to transmit data to the target. The protocol used by this layer is IP

IP itself is a message oriented and unreliable protocol. Each time we transmit data, it will help us choose the path, but it is not consistent. If a path error occurs during transmission, select another path; However, if data loss or error occurs, it cannot be solved. That is, the IP protocol cannot cope with data errors

TCP/UDP layer

TCP and UDP layers complete the actual data transmission based on the path information provided by IP layer, so this layer is also called Transport layer

The IP layer only focuses on the transmission process of one packet. Therefore, even if multiple data packets are transmitted, each data packet is actually transmitted by the IP layer, that is, the transmission sequence and transmission itself are unreliable. If only the IP layer is used to transmit data, the later transmitted packet B may arrive earlier than the first transmitted packet A, or A packet may be damaged.

To sum up, TCP and UDP exist on the IP layer, which determines the data transmission mode between hosts. After the TCP protocol is confirmed, it gives reliability to the unreliable IP protocol

application layer

The selection of data transmission path and data confirmation process are chanted into the socket. Programmers do not need to consider these processes. We only need to use the socket to program. In the process of writing software, the data transmission rules (mode) between server and client need to be determined according to the characteristics of the program, which is the application layer protocol. Most of the content of network programming is to design and implement application layer protocols

Implement server / client based on TCP

Default function call order on TCP server side

  1. socket(). Create socket
  2. bind(). Assign socket address
  3. listen(). Waiting for connection request status
  4. accept(). Allow connection
  5. rand()/write(). Data exchange
  6. close(). Disconnect

Enter the state of waiting for connection request

Call the listen function to enter the state of waiting for connection request. Only when the listen function is called can the client enter the state where connection requests can be issued. That is to say, the connect function can be invoked by the client at the moment.

#include <sys/socket.h>

int listen(int sock, int backlog);

Returns 0 on success and - 1 on failure. sock: socket file descriptor that wants to enter the state of waiting for connection request. The passed descriptor socket parameter becomes server-side socket (listening socket); backlog: the length of the connection request Queue. If it is 5, the Queue length is 5, which means that up to 5 connection requests can be queued

The second parameter value of the listen function is related to the characteristics of the server. For example, the Web server that receives requests frequently should be at least 15

"The server side is waiting for a connection request" means that when the client requests a connection, the request is always waiting before accepting the connection

Accept client connection request

After calling the listen function, if there is a new connection request, it shall be accepted in order. Accepting a request means entering a state of acceptable data. The server socket is the gatekeeper, so another socket is required to enter this state, but it is not necessary to create it yourself. The accept function automatically creates the socket and connects to the requesting client

#include <sys/socket.h>

int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);

Returns the created socket file descriptor on success and - 1 on failure. sock: file descriptor of server socket; Addr: save the variable address value of the client address information initiating the connection request, and fill the client address information into the address variable parameter passed after calling the function; Address: the second parameter is the length of the addr structure, but there is a variable address of length. After the function call is completed, the variable is filled in the client address length

The accept function accepts the connection request waiting for the client connection request to be processed in the queue. When the function call is successful, the accept function will generate a socket for data I/O and return its file descriptor

hello world server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unisted.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);                                  // Create a socket that is not yet a real server-side socket
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)                                               // At this time, the socket is the server-side socket
    {
        error_handling("listen() error");
    }

    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); // Take a connection request from the queue head to establish a connection with the client, and return the created socket file descriptor
    if (clnt_sock == -1)
    {
        error_handling("accept() error");
    }

    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Default function call order for TCP clients

  1. socket(). Create socket
  2. connect(). Request connection
  3. read()/write(). Exchange data
  4. close(). Disconnect

After the server calls the listen function, a connection request waiting queue is created, and then the client can request a connection. Use the connect function to initiate a connection request

#include <sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);

Returns 0 on success and - 1 on failure. sock: client socket file descriptor; Servaddr: save the variable address value of the address information of the target server; addrlen: pass the address variable length passed to the second struct parameter servaddr in bytes

After the client calls the connect function, it will not return (complete the function call) until one of the following occurs:

  1. The server receives the connection request
  2. The connection request is interrupted due to abnormal conditions such as network disconnection

The so-called "receive connection" does not mean that the server calls the accept function. In fact, the server records the connection request information to the waiting queue. Therefore, data exchange does not occur immediately after the connect function returns

Address information of client socket

The socket address assignment does not occur during the client implementation, but the connect function is called immediately after the socket is created. Because the IP address and port of the client are automatically allocated when the connect function is called, there is no need to call the marked bind function for allocation

When: when the connect function is called; Where: in the operating system kernel; How to: use the IP of the computer (host) and the port is random

Hello world client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)  // Call the connect function to send a connection request to the server
    {
        error_handling("connect() error!");
    }

    str_len = read(sock, message, sizeof(message) - 1);                        // After completing the connection, receive the data transmitted by the server
    if (str_len == -1)
    {
        error_handling("read() error!");
    }

    printf("Message from server : %s \n", message);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Server / client function call relationship based on TCP

After the server creates the socket, it continuously calls the bind and listen functions to enter the waiting state, and the client initiates the connection request by calling the connect function. It should be noted that the client can only call the connect function after the server calls the listen function. At the same time, before the client calls the connect function, the server may call the accept function first. At this time, the server enters the blocking state when calling the accept function until the client calls the connect function

Implement iterative server / client

Implement iterative server side

If the server exits after processing a client connection request, the connection request waiting queue is not meaningful. In fact, the server should provide services to all clients after setting the size of the waiting queue. If you want to continue to accept subsequent client connection requests, the simplest way is to insert a circular statement and call the accept function repeatedly. The calling sequence is as follows:

After calling the accept function, we immediately call the I/O related read and write functions, and then call the close function for the client.

Iterative echo server / client

Echo server / client, that is, the server sends the string data transmitted by the client back to the client intact, just like echo

Basic operation mode of the program:

  1. The server is connected to only one client at the same time and provides echo service
  2. The server provides services to five clients in turn and exits
  3. The client receives the string entered by the user and sends it to the server
  4. The server sends the received string data back to the client, i.e. "echo"
  5. The string echo between the server and the client is executed until the client enters Q
Echo server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char* message);

int main(int argc, char* argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    clnt_adr_sz = sizeof(clnt_adr);

    for(i = 0; i < 5; i++)                                                           // Loop statement added to handle 5 client connections
    {
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1)
        {
            error_handling("accept() error");
        }
        else
        {
            printf("Connected client %d \n", i+1);
        }

        while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)                  // The read string is transmitted intact. If EOF is received, the client disconnects and exits the loop
        {
            write(clnt_sock, message, str_len);
        }

        close(clnt_sock);                                                            // Call the close function for the socket and send EOF to the corresponding socket connected
    }
    close(serv_sock);
    return 0;
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
Echo client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char* message);

int main(int argc, char* argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;

    if (argc != 3)
    {
        printf("Usage: %s <IP> <prot>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("connect() error!");
    }
    else
    {
        puts("Connected..........");
    }

    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        {
            break;
        }

        write(sock, message, strlen(message));        // Passing data in strings
        str_len = read(sock, message, BUF_SIZE-1);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(sock);                                      // Call the close function to send EOF to the corresponding socket
    return 0;
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
Problems with echo client

Each time the read and write functions are called, the actual I/O operations will be performed in string units. Because TCP has no data boundary, the string passed by calling the write function multiple times may be passed to the server at one time. At this time, the client may receive multiple strings from the server

It is also possible that the server wants to transfer data through the write function once, but if the data is too large, the operating system may divide the data into multiple packets and send them to the client. In addition, during this process, the client may call the read function before receiving all packets

Windows based implementation

Key points of converting examples on Linux platform into examples on Windows Platform:

  1. Initialize and clear socket related libraries through WSAStartup and WSACleanup functions
  2. Switch the data type and variable name to Windows style
  3. recv and send functions instead of read and write functions in data transmission
  4. When closing a socket, use the closesocket function instead of the close function
Implement echo server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>

#define BUF_SIZE 1024
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	char message[BUF_SIZE];
	int strLen, i;

	SOCKADDR_IN servAdr, clntAdr;
	int clntAdrSize;
	
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}
	
	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}

	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error");
	}

	clntAdrSize = sizeof(clntAdr);

	for (i = 0; i < 5; i++)
	{
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
		if (hClntSock == -1)
		{
			ErrorHandling("accept() error");
		}
		else
		{
			printf("Connected client %d \n", i + 1);
		}

		while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
		{
			send(hClntSock, message, strLen, 0);
		}
		closesocket(hClntSock);
	}
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
Implement echo client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#define BUF_SIZE 1024
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	char message[BUF_SIZE];
	int strLen;
	SOCKADDR_IN servAdr;

	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error");
	}

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	inet_pton(AF_INET, argv[1], &servAdr.sin_addr);
	servAdr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error!");
	}
	else
	{
		puts("Connected.............");
	}

	while (1)
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);

		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
		{
			break;
		}

		send(hSocket, message, strlen(message), 0);
		strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
		message[strLen] = 0;
		printf("Message from server: %s", message);
	}
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Tags: socket computer networks tcp udp TCP/IP

Posted on Sat, 25 Sep 2021 05:37:22 -0400 by tyrnt