Linux–TCP protocol and related socket programming

1. TCP protocol concept

Like the UDP protocol, the TCP protocol is also a protocol applied at the transport layer. Although they are all applied at the transport layer, the usage methods and application scenarios are quite different. The TCP protocol has the characteristics of connection (reliable) and byte stream oriented.

(1). There is a connection

The so-called connection means that the transmission of information is based on the fact that the two parties in communication have “established contact” in advance, which can be understood as the creation of a channel for communication in advance. Moreover, TCP communication is directional, and the information sent can only be sent in a “point-to-point” manner. However, sending information under the UDP protocol does not require a connection. For details, please refer to this article: UDP protocol and related socket programming

Because the communication of TCP is based on the connection between the two parties, there is no problem of data loss. If the receiver does not receive the message, the sender will continue to send data until the receiver gets it. This is why network communication under the TCP protocol is reliable.

(2). Oriented to byte stream

Like the system input and output stream, the TCP protocol also has a buffer. When transmitting data, if the data is too short, it will be stored in the buffer until the data length reaches the sending requirement and then sent to the receiver. If the data is too long, it will be split, part by part into the buffer and then sent. It should be noted here that because of the existence of the buffer, a piece of data may be disassembled and sent, so that the data received by the receiver may not be complete at one time, so it is necessary to customize the communication protocol (check whether the data is complete) to ensure the target The data is used after it is complete, and the specific method will be explained in the socket programming code. The UDP protocol is based on the characteristics of datagrams, and there is no concept of a buffer zone, so the integrity of the received data can be guaranteed.

2.TCP socket programming

(1). System call interface

①Get socket/create socket structure/binding

The relevant interfaces and usage methods have been introduced in the blog of UDP socket programming, so I won’t repeat them here: UDP protocol and related socket programming

The usage of TCP is basically the same as that of UDP, with slightly different parameters.

Get the socket:

The second parameter of the socket function is the socket type, and TCP is byte-oriented, that is, SOCK_STREAM. It should be noted that the TCP protocol has two socket file descriptors, and the socket interface returns called the listening socket. Used to obtain connections; there is also a service socket used to communicate with the peer network.

int listensocket = -1;
listensocket = socket(AF_INET, SOCK_STREAM, 0);

Create the structure:

It is consistent with the way UDP creates the structure.

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr. sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;

bind:

Consistent with the UDP binding method

int i = bind(_listensocket, (struct sockaddr *) & amp;addr, sizeof(addr));
if (i < 0) ...

②Set the monitoring status

Because the TCP protocol is specific to connectivity, it is necessary to call the listen interface to set a listening state in advance.

The first parameter of the listen interface is the listening socket.

The second parameter is the maximum number of connections. The peers who establish connections with TCP will be recorded in a queue. This parameter defines the maximum length of the queue (the queue length is the parameter value + 1). When the queue length reaches the maximum, if another peer wants to establish a connection, an error will be returned. The reference is as follows:

If the monitoring is set up successfully, it will return 0, and if it fails, it will return -1.

int i = listen(listensocket, 20);
if(i < 0) ...

③The server is waiting for connection

The system interface accept is used to obtain a connection with the peer. If there is no host connected to this end, it will be blocked on the accept function.

In terms of parameters, the first one is the listening socket.

The second and third are output parameters, which are the same as the recvfrom interface, and are used to obtain the structure struct sockaddr that records the IP address and port number in the peer.

When the peer tries to establish a TCP connection with the local machine, if the accept is successful, the service socket will be returned, and the subsequent network communication behaviors will be completed using the service socket. If accept fails, it will return -1, as shown below:

struct sockaddr_in addr;//Used to obtain the peer IP address & amp; port number
bzero( & amp; addr, sizeof addr);
socklen_t addr_len;
int servesocket = accept(listensocket, (struct sockaddr*) &addr, &addr_len);
if(servesocket < 0) ...

④Client connection

After the server uses accept to block, the client needs to call the connect interface to actively establish a connection with the server.

It should be noted that, like the UDP protocol, the client does not need to manually bind its own port number and IP address before establishing a connection, and the server can obtain it automatically by the system.

In terms of interface parameters, the first parameter is the local (client) socket file descriptor (the return value of the socket function).

The second parameter is the struct sockaddr type, which records the server IP address and port number, which is used to determine which server to connect to.

The third parameter records the length of the second parameter object.

Returns 0 if the connection is successful, and -1 if it fails.

int sock = socket(...);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//Record the port number of the server
addr.sin_addr.s_addr = inet_addr(ip);//Record the IP address of the server
int i = connect(sock, (struct sockaddr*) & addr, sizeof addr);
if(i < 0) ...

⑤Send data

Call the write/send system interface to send data.

Because the service socket is essentially a file descriptor, it can directly use the write interface to send data, and the specific method is the same as that of ordinary files.

The use of the send interface and the write interface is not much different. The first three parameters have the same meaning, and the last parameter represents the sending strategy, which is generally filled with 0.

But one thing needs to be emphasized, the first parameter of write and send is the service socket on the server side, because the service socket of each client (opposite end) is different, and the service socket can be used to identify the unique client. On the client side, the socket file descriptor returned by the socket interface can be used.

char buf[1024];
...//Produce network data
//**********Server
int i = send(servesocket, buf, strlen(buf), 0);
//or: write(servesocket, buf, strlen(buf));

//**********client
int i = send(sock, buf, strlen(buf), 0);
//or: write(sock, buf, strlen(buf));
if(i < 0) ...

⑥Get data

Call the recv/read interface to obtain data.

The reason is the same as sending data, so I won’t repeat it. After reading the example, you will understand:

char buf[1024];
memset(buf, 0, sizeof buf);
//***********Server
int i = recv(servesocket, buf, sizeof buf, 0);
//or: read(servesocket, buf, sizeof buf);
//************client
int i = recv(sock, buf, sizeof buf, 0);
//or: read(sock, buf, sizeof buf);
if(i <= 0) ...

But here is a doubt:

First of all, we know that in UDP protocol communication, when receiving data, the maximum receiving length is specified to be buf size -1. This is to prevent the received data from exceeding the size of buf, resulting in the inability to set the last bit as ‘\0’:

int i = recvfrom(sockfd, buf, sizeof buf - 1, ...);
buf[i] = 0;

Then why is it no longer specified that the receiving length is size – 1 when receiving data using the TCP protocol?

This is because the TCP protocol has the characteristics of byte stream-oriented, that is, when the first part introduces byte stream-oriented, the data in one transmission may be incomplete and multiple transmissions are required. For example, if the client sends the string “abcdefg”, but the TCP buffer is too small and only efg is stored, then the server accepts abcd for the first time, and can only receive it the second time. efg. So the receive length should be buf size.

So how to judge when all the data has been received? A custom communication protocol is required, which will be explained in the next section.

(2). Simulate TCP server and client

①customized agreement

Based on the above, we know that a piece of data in TCP protocol communication may be sent in batches multiple times. Therefore, it is necessary to judge according to the data content which data transmission is completed after receiving.

How to judge? We can do this: add “data length”\r\\
” at the beginning of a piece of data, and add “\r\\
” at the end.

Example: 5\r\\

abcde\r\\

When receiving data, first receive the data length and \r\\
, know that there is a new piece of data coming and know the data length. When \r\\
is received again, a copy of the data is completely transmitted. Of course, this method can also be understood as adding headers and trailers to the data.

#define SEP "\r\\
"
#define SEP_LEN strlen(SEP)
string Decode(string *str) // Release the custom protocol (get data)
{
    int pos = str->find(SEP);
    if (pos == string::npos)
        return "";
    int len = atoi(str->substr(0, pos).c_str());
    int size = str->size() - pos - SEP_LEN * 2;
    if (size >= len)
    {
        str->erase(0, pos + SEP_LEN);
        string ret = str->substr(0, len);
        str->erase(0, len + SEP_LEN);
        return ret;
    }
    else
        return "";
}
bool Encode(string *buf) // Add custom protocol length\r\\
xxxxxxx\r\\

{
    string ret = std::to_string(buf->size());
    ret + = SEP;
    ret + = *buf;
    ret + = SEP;
    *buf = ret;
    return true;
}

②Serialization and deserialization

Of course, there is a doubt in the above. If the data content is not a string, such as a structure, how should the protocol be customized? The answer to this question is very simple. Usually, in network communication, all the data to be transmitted to the network can be converted into strings. In layman’s terms, for example, a structure converts each data member into a string, and this process is called serialization. The process of converting string-type data in the network into a target type in the host (such as converting a string into a structure) is called deserialization.

The diagram is as follows:

③Server

The server has three main tasks: establish TCP communication, receive data, and send data.

Of course, in addition to the need to establish monitoring at the TCP server, after connecting to a client, a child process needs to be created to be responsible for communicating with the client, and the parent process continues to wait for new clients to connect in a loop. In addition to the above methods to complete multi-client connections, multi-threading methods can also be used. If you use the multi-process version, you need to pay attention. The child process needs to close the listening socket because the child process is not used to obtain connections; the parent process must close the service socket because the parent process is only used to establish a connection with the client. If If it is not closed, as the number of connected clients increases, the problem of insufficient file descriptors will occur.

After receiving the data, first remove the custom protocol from the data in the network, that is, remove the header, and then deserialize the data of the string type into the host object type. When the data is processed and returned to the client, the data is first serialized into a string type for network transmission, and then a custom protocol (header) is added. Of course, the service socket returned by accept is used for communication.

The pseudocode is as follows:

class Server
{
    static void serveFunc(int sock, string ip, uint16_t port) // used for child process and client communication
    {
        ...//Receive data (remove protocol + deserialize), process data, send data (add protocol + serialize)
    }

public:
    ...
    void initServer()
    {
        ...//Get listening socket, create structure, bind
        listen(_listensocket, 20);//listen
    }
    void startServer()
    {
        signal(SIGCHLD, SIG_IGN);//Separate the parent and child processes so that the child process can be automatically recycled instead of becoming a zombie process
        while (true)//parent process loop, waiting for multiple client connections
        {
            ...//Create the structure of the receiving client
            int servesocket = accept(_listensocket, (struct sockaddr *) & amp;addr, & amp;len);//Connect to the client and get the service socket
            if (fork() == 0)//The child process is responsible for communicating with a specific client
            {
                close(_listensocket);//The child process communication does not need to listen to the socket
                serveFunc(servesocket, ip, port);//communication function
                exit(0);//The child process exits
            }
            close(servesocket);//The parent process does not need a service socket
        }
    }

private:
    string_IP;
    uint16_t _port;
    int _listensocket;
};

④Client

The client has three tasks: actively establish a connection with the server, send data, and obtain data.

The connect interface is used to establish a connection with the server. The client that communicates with UDP does not need to bind the IP and port number itself, but let the operating system automatically bind (avoid multiple servers on a host to bind the same IP address at the same time. and port number).

The idea of sending data and obtaining data is the same as that of the server. Before sending data, serialize and then add the protocol. After obtaining the data, remove the protocol and then deserialize. When communicating, use the socket function to return the value.

The pseudocode is as follows:

int main(int argc, char* argv[])
 {
    ...//Get the socket, create a structure according to the server IP and port number (for connect)
    //No need to bind
    //Establish a connection with the server
    int i = connect(sock, (struct sockaddr*) & addr, sizeof addr);
    ...//Communicate with the server, send data, receive data
    close(sock);
    return 0;
 }

Please correct me if there is any mistake