Network programming socket (Part 2)

Table of contents

1. TCP network program

1.1 Server initialization

1.1.1 Create socket

1.1.2 Server Binding

1.1.3 Server monitoring

1.2 Server startup

1.2.1 The server obtains the connection

1.2.2 Server processing request

1.3 Client initialization

1.4 Client startup 

1.4.1 Initiate connection

1.4.2 Initiate a request

1.5 Network Test

1.6 Disadvantages of single execution stream server

Two, multi-process version TCP network program

2.1 There are problems

2.2 Capture SIGCHLD signal

2.3 The grandson process provides services

3. Multi-threaded version of TCP network program

4. Thread pool version TCP network program

5. Address conversion function

5.1 String IP to integer IP

5.2 Integer IP to string IP

5.3 inet_ntoa function problem


1. TCP network program

1.1 Server initialization

1.1.1 Create socket

When the TCP server calls the socket function to create a socket, the parameters are set as follows:

  • The protocol family selects AF_INET for network communication
  • The service type required when creating a socket should be SOCK_STREAM, because the TCP server is written, and SOCK_STREAM provides an ordered, reliable, full-duplex, connection-based streaming service
  • The protocol type is set to 0 by default

If the file descriptor obtained after creating the socket is less than 0, the socket creation fails. At this time, there is no need to perform subsequent operations, just terminate the program directly

class TcpServer
{
public:
    void InitSrever();
    ~TcpServer();
private:
    int _socket_fd;
};

void TcpServer::InitSrever()
{
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        exit(1);
    }
    cout << "socket success" << endl;
}

TcpServer::~TcpServer() { if(_socket_fd >= 0) close(_socket_fd); }
  • The method of creating a socket by a TCP server is basically the same as that of a UDP server, but when creating a socket, TCP uses a streaming service, while UDP uses a user datagram service
  • When the server is destroyed, the file descriptor corresponding to the server can be closed

1.1.2 Server Binding

After the socket is created, only a file is opened at the system level. The file is not associated with the network, so after the socket is created, the bind function needs to be called to perform the binding operation.

The binding steps are as follows:

  • Define the struct sockaddr_in structure variable, and fill the attribute information related to the server network into the variable, such as protocol family, IP address, port number, etc.
  • When filling in the attribute information related to the server network, the corresponding protocol family is AF_INET, and the port number is the port number of the current TCP server program. When setting the port number, you need to call the htons() function to convert the port number from the host sequence to the network sequence
  • When setting the IP address of the server, it can be set to local loopback 127.0.0.1, which means local communication. It can also be set as a public network IP address, indicating network communication
  • If you are using a cloud server, you don’t need to bind a fixed IP address when setting the IP address of the server. You can directly set the IP address to INADDR_ANY. At this time, the server can read data from any local network card. INADDR_ANY is essentially 0, so there is no need to convert the network byte order when setting
  • After filling in the attribute information related to the server network, call the bind function to bind. Binding is actually to associate files with the network. If the binding fails, there is no need to perform follow-up operations, just terminate the program directly.

The port number of the server is required when the TCP server is initialized, so the port number needs to be introduced in the server class, and a port number needs to be passed in when the server object is instantiated. Since the cloud server is currently used, the public network IP address is not bound when binding the IP address of the TCP server, and INADDR_ANY can be bound directly, so the following code does not introduce the IP address into the server class

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_fd(-1),_server_port(port) {}
    void InitSrever();
    ~TcpServer();
private:
    int _socket_fd;
    uint16_t _server_port;
};

void TcpServer::InitSrever()
{
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        exit(1);
    }
    cout << "socket success" << endl;
    //绑定
    struct sockaddr_in local;
    memset(&local, '\0', sizeof local);
    local.sin_family = AF_INET;
    local.sin_port = htons(_server_port);
    local.sin_addr.s_addr = INADDR_ANY;
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof local) < 0) {
        cerr << "bind fail" << endl;
        exit(2);
    }
    cout << "bind success" << endl;
}

1.1.3 Server monitoring

The initialization operation of the UDP server has only two steps, creating a socket and binding. The TCP server is connection-oriented. Before the client officially sends data to the TCP server, it needs to establish a connection with the TCP server before communicating with the server.

Therefore, the TCP server needs to always pay attention to whether there is a connection request from the client. At this time, it is necessary to set the socket created by the TCP server to the listening state.

int listen(int sockfd, int backlog);
  • sockfd: the file descriptor corresponding to the socket that needs to be set to the listening state
  • backlog: The maximum length of the fully connected queue. If multiple clients send connection requests at the same time, the connections that have not been processed by the server will be put into the connection queue. This parameter represents the maximum length of the full connection queue. Generally, do not set it too large, set it to 5 or 10 is enough

Return value: 0 is returned if the monitoring succeeds, -1 is returned if the monitoring fails, and errno is set at the same time

Server monitoring code implementation

If the monitoring fails, there is no need to perform follow-up operations, because the failure of monitoring means that the TCP server cannot receive the connection request from the client, and the program can be terminated directly

void TcpServer::InitSrever()
{
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        exit(1);
    }
    cout << "socket success" << endl;
    //绑定
    struct sockaddr_in local;
    memset(&local, '\0', sizeof local);
    local.sin_family = AF_INET;
    local.sin_port = htons(_server_port);
    local.sin_addr.s_addr = INADDR_ANY;
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof local) < 0) {
        cerr << "bind fail" << endl;
        exit(2);
    }
    cout << "bind success" << endl;
    //设置服务器监听状态
    if(listen(_socket_fd, BACKLOG) < 0) {
        cerr << "listen fail" << endl;
        exit(3);
    }
    cout << "listen success" << endl;
}

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port) {}
    void InitSrever();
    ~TcpServer();
private:
    int _socket_listen_fd;
    uint16_t _server_port;
};
  • The socket created when initializing the TCP server is not an ordinary socket, but should be called a listening socket. In order to show the meaning, change the name of the socket in the code from _socket_fd to _socket_listen_fd
  • When initializing the TCP server, the initialization of the TCP server is considered complete only when the socket is successfully created, bound, and monitored successfully.

1.2 Server startup

1.2.1 The server obtains the connection

The TCP server can start running after initialization, but before the TCP server communicates with the client network, the server needs to obtain the connection request from the client first.

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: a specific listening socket, which means to get a connection from the listening socket
  • addr: attribute information related to the peer network, including protocol family, IP address, port number, etc., output parameters
  • addrlen: The length of the addr structure that is expected to be read is passed in when calling, and the length of the addr structure that is actually read is represented when it is returned. This is an input and output parameter

Return value: If the connection is successfully obtained, the file descriptor of the received socket will be returned, and -1 will be returned if the connection fails, and the error code will be set at the same time

What is the socket returned by the accept function?

When the accept function is called to obtain a connection, it is obtained from the listening socket. If the accept function obtains the connection successfully, it will return the file descriptor corresponding to the received socket

The role of the listening socket and the socket returned by the accept function:

  • Listening socket : used to obtain connection requests from clients. The accept function will continuously obtain new connections from the listening socket
  • The socket returned by the accept function: used to provide services for the connection obtained by this accept. The task of listening to the socket is to continuously obtain new connections, and the socket that actually provides services for these connections is the socket returned by the accept function

The server obtains the connection code implementation

  • The accept function may fail to obtain a connection, but the TCP server will not exit due to a failure to obtain a connection, so the server should continue to obtain a connection after failing to obtain a connection
  • To output the obtained connection corresponding to the client's IP address and port number information, you need to call the inet_ntoa function to convert the integer IP into a string IP (to a host sequence), and call the ntohs function to convert the port number from a network sequence to a host sequence
  • The inet_ntoa function actually does two tasks at the bottom, one is to convert the network sequence into a host sequence, and the other is to convert the integer IP of the host sequence into a string-style dotted decimal IP
void TcpServer::StartUp()
{
    //获取连接
    while(true) 
    {
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
    }
}

1.2.2 Server processing request

The TCP server has been able to obtain the connection request, and the obtained connection needs to be processed next. It is not the listening socket that provides services to the client, because the listening socket will continue to obtain the next request connection after obtaining a connection. The socket that provides services to the corresponding client is actually the socket returned by the accept function. Hereinafter referred to as "service socket"

In order to allow both communication parties to see the corresponding phenomenon, an echo TCP server is implemented below. When the server provides services for the client, it outputs the data sent by the client and resends the data sent by the client to the client. client. When the client receives the response data from the server and prints out the data, it can ensure that the server and the client can communicate normally.

read function

ssize_t read(int fd, void *buf, size_t count);
  • fd: A specific file descriptor, indicating that data is read from the file descriptor
  • buf: the storage location of the data, indicating that the read data is stored in this location
  • count: the number of data, indicating the number of bytes of data read from the file descriptor
  • If the return value is greater than 0, it means the number of bytes actually read this time
  • If the return value is equal to 0, it means that the peer has closed the connection
  • If the return value is less than 0, it means that an error occurred while reading

If the client closes the connection, then the server will read 0 after reading the information in the socket at this time, so if the return value of the server after calling the read function is 0, the server does not need to Serve this client again

write function

ssize_t write(int fd, const void *buf, size_t count);
  • fd: A specific file descriptor, indicating that data is written to the socket corresponding to the file descriptor
  • buf: data to be written
  • count: the number of bytes that need to be written

If the write is successful, it will return the number of bytes actually written, and if the write fails, it will return -1, and the error code will be set at the same time.
When the server calls the read function to receive the data from the client, it can then call the write function to respond to the data. client

Server-side processing request code implementation

The data read by the server is read from the service socket, and when the data is written, it is also written into the service socket. The service socket can both read data and write data, which is the embodiment of TCP full-duplex communication

When reading the data sent by the client from the service socket, if the return value obtained after calling the read function is 0, or the reading error occurs, you should directly write the file descriptor corresponding to the service socket closure. Because file descriptors are essentially array subscripts, the resources of file descriptors are limited. If they are occupied all the time, there will be fewer and fewer available file descriptors. Therefore, the corresponding files should be closed in time after serving the client. descriptor, otherwise it will cause a file descriptor leak

void TcpServer::StartUp()
{
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        Service(server_socket_fd, client_ip, client_port);
    }
}

void TcpServer::Service(int server_socket_fd, string client_ip, uint16_t client_port)
{
    char buffer[BUFF_SIZE];
    while(true) 
    {
        ssize_t size = read(server_socket_fd, buffer, sizeof(buffer) - 1);
        if(size > 0) //读取成功
        {
            buffer[size] = '\0';
            cout << buffer << endl;
            write(server_socket_fd, buffer, size);
        }
        else if(size == 0) //对端关闭连接
        {
            cout << client_ip << ":" << client_port << " close" << endl;
            break;
        }
        else //读取失败
        {
            cerr << server_socket_fd << " read error" << endl;
            break;
        }
    }
    close(server_socket_fd);
    cout << client_ip << ":" << client_port << "server done" << endl;
}

1.3 Client initialization

create socket

The client does not need to bind and listen:

  • The server needs to be bound because the IP address and port number of the server cannot be changed at will. Although the client also needs an IP address and port number, the client does not need the programmer to manually perform the binding operation. When the client connects to the server, the system will automatically assign a port number to the client.
  • The server needs to monitor because the server needs to obtain new connections through monitoring, but no one will actively connect to the client, so the client does not need to monitor

The client must know the IP address and port number of the server to be connected, so in addition to having its own socket, the client also needs to know the IP address and port number of the server, so that the client can pass through the socket. Communicate to the specified server

class TcpClient
{
public:
    TcpClient(string ip, uint16_t port):_socket_fd(-1),_server_ip(ip),_server_port(port) {}
    void InitClient();
    ~TcpClient();
private:
    int _socket_fd;
    string _server_ip;
    uint16_t _server_port;
};

void TcpClient::InitClient()
{
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        exit(1);
    }
}

TcpClient::~TcpClient() { if(_socket_fd >= 0) close(_socket_fd); }

1.4 Client startup 

1.4.1 Initiate connection

The client does not need to bind or monitor. After the client creates the socket, it can directly initiate a connection request to the server.

connect function

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: a specific socket, indicating that a connection request is initiated through this socket
  • addr: attribute information related to the peer network, including protocol family, IP address, port number, etc.
  • addrlen: the length of the incoming addr structure

Return value: 0 is returned if the connection or binding is successful, and -1 is returned if the connection fails, and the error code will be set at the same time

client connection server code

It is not that the client does not need to bind, but the programmer does not need to manually perform the binding operation. When the client initiates a connection request to the server, the system will randomly assign a free port number to the client for binding. Because both communication parties must have an IP address and port number, otherwise the communication parties cannot be uniquely identified. If the connect function is successfully called, the client will randomly bind a port number to the client and send it to the peer server

When calling the connect function to initiate a connection request to the server, you need to pass in the corresponding network information of the server, otherwise the connect function does not know which server the client is going to initiate a connection request to

void TcpClient::StartUp()
{
    //发起连接
    struct sockaddr_in server;
    memset(&server, '\0', sizeof server);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(_server_ip.c_str());
    server.sin_port = htons(_server_port);
    if(connect(_socket_fd, (struct sockaddr*)&server, sizeof(server)) == 0) {
        cout << "connect success" << endl;
        Request(); //发起请求
    }
    else {
        cerr << "connect fail" << endl;
        exit(2);
    }
}

1.4.2 Initiate a request

When the client is connected to the server, the client can send data to the server, allowing the client to send the data entered by the user to the server, and call the send function to write data to the socket when sending

After the client sends the data to the server, since the server will echo the data after reading the data, the client needs to call the recv function to read the response data from the server after sending the data, and then process the response data Print to confirm that the communication between the two parties is correct

void TcpClient::Request()
{
    char buffer[BUFF_SIZE];
    string message;
    while(true) 
    {
        cout << "Pleses Entre#";
        getline(cin, message);
        send(_socket_fd, message.c_str(), message.size(), 0);
        ssize_t size = recv(_socket_fd, buffer, sizeof(buffer) - 1, 0);
        if (size > 0){
			buffer[size] = '\0';
			cout << "server echo# " << buffer << endl;
		}
		else if (size == 0) {
			cout << "server close!" << endl;
			break;
		}
		else {
			cerr << "read error!" << endl;
			break;
		}
    }
}

A client object can be constructed through the IP address and port number of the server

void Usage(std::string proc)
{
	cout << "Usage: " << proc << "server_ip server_port" << endl;
}
int main(int argc, char* argv[])
{
	if (argc != 3) {
		Usage(argv[0]);
		exit(1);
	}
	string server_ip = argv[1];
	int server_port = atoi(argv[2]);

	TcpClient* client = new TcpClient(server_ip, server_port);
	client->InitClient();
	client->StartUp();
	return 0;
}

1.5 Network Test

Both the server and the client have been written, and the network test is performed below. When testing, start the server first, and then use the netstat command to view it. At this time, you can see the ./server service process, which is currently in the listening state

Then run the client in the form of ./client IP number and port number . At this time, the client will initiate a connection request to the server, and the server will provide services for the client after obtaining the request.

When the client sends a message to the server, the server can identify the corresponding client through the printed IP address and port number, and the client can also judge whether the server has received the message sent by the server through the message returned by the server. information

If the client exits at this time, the return value is 0 when the server calls the read function. At this time, the server knows that the client has exited, and then terminates the service to the client.

At this time, the server terminates the service to the client, but the server does not terminate, and is still running, waiting for the next connection request from the client.

1.6 Disadvantages of single execution stream server

When only one client is connected to the server, the client can enjoy the service of the server normally

But when this client is enjoying the service of the server, another client also connects to the server. At this time, although the client shows that the connection is successful, the server does not show that there is a new connection, and the client sends to the server. The message is not printed on the server, nor is the server echoing the data to the client

After the first client exits, the server will print the data sent by the second client and echo it to the second client

Single execution stream server

As can be seen from the above figure, the server will only serve another client after serving one client. Because what is currently written is a single-execution streaming server

When the server calls the accept function to obtain a connection, it will provide services to the client. However, during the service provided by the server, other clients may initiate connection requests, but the connection of the next client will not be accepted until the current client is served. request, causing the server to only serve one client at a time

Why does the client show that the connection is successful?

When the server is providing services to the first client, the connection request initiated by the second client to the server is successful, but the server does not call the accept function to obtain the connection

In fact, a connection queue will be maintained at the bottom layer, and new connections that are not accepted by the server will be placed in this connection queue, and the maximum length of this connection queue is specified by the second parameter of the listen function, so although the server does not have Obtain the connection request sent by the second client, but the second client shows that the connection is successful

solution

A server with a single execution flow can only provide services to one client at a time. At this time, the resources of the server are not fully utilized, so the server is generally not written as a single execution flow. To solve this problem, it is necessary to change the server to multi-execution stream, and at this time, it is necessary to introduce multi-process or multi-thread

Two, multi-process version TCP network program

2.1 There are problems

When the server calls the accept function to obtain a new connection, instead of the current execution flow serving the client corresponding to the connection, the current execution flow calls the fork function to create a child process, and the child process provides services for the connection obtained by the parent process.

Since the parent and child processes are two different execution flows, when the parent process calls fork to create a child process, the parent process can continue to obtain new connections from the listening socket, regardless of whether the client corresponding to the obtained connection is serving complete

The child process inherits the file descriptor table of the parent process

The file descriptor table belongs to a process, and the child process will inherit the file descriptor table of the parent process after it is created. If the parent process opens a file, the file descriptor corresponding to the file is 3, and the file descriptor No. 3 of the child process will also point to the opened file. If the child process creates another child process, then the No. 3 file descriptor of the grandchild process The character will also point to this open file

After the parent process creates a child process, the independence between the parent and child processes is maintained. At this time, changes in the file descriptor table of the parent process will not affect the child process. For example, when a parent-child process uses an anonymous pipe to communicate, the parent process first calls the pipe function to obtain two file descriptors, one is the file descriptor at the read end of the pipe, and the other is the file descriptor at the write end of the pipe. At this time, the parent process creates The child process will inherit these two file descriptors. After that, one of the parent and child processes closes the read end of the pipeline, and the other closes the write end of the pipeline. At this time, the changes in the file descriptor table of the parent and child processes will not affect each other. After that, the parent and child processes One-way communication can be carried out through this pipeline

The same is true for the socket file. The child process created by the parent process will also inherit the socket file of the parent process. At this time, the child process can read and write the specific socket file, and then complete the corresponding client. Serve

Waiting for child process problem

When the parent process creates a child process, the parent process needs to wait for the child process to exit, otherwise the child process will become a zombie process, causing a memory leak. Therefore, after the server creates a child process, it needs to call the wait or waitpid function to wait for the child process

Blocking wait and non-blocking wait:

  • If the server waits for the child process in a blocking manner, the server still needs to wait for the current client to be served before continuing to obtain the next connection request. At this time, the server still provides services to the client in a serial manner
  • If the server waits for the child process in a non-blocking manner, although the server can continue to obtain new connections while the child process is providing services to the client, the server needs to save the PIDs of all child processes at this time, and it needs to continue to spend Time checks whether the child process exits, and the coding is more complicated

The server has to wait for the child process to exit, whether it uses blocking waiting or non-blocking waiting, it is not satisfactory. At this point, you can consider the solution that the server does not wait for the child process to exit

2.2 Capture SIGCHLD signal

When the child process exits, it will send a SIGCHLD signal to the parent process. If the parent process captures the SIGCHLD signal and sets the processing action of the signal to ignore, the parent process can continue to obtain new connections from the listening socket.

This scheme is relatively simple to implement and is recommended

void TcpServer::StartUp()
{
    //设置忽略SIGCHLD信号
    signal(SIGCHLD, SIG_IGN);
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        pid_t id = fork();
        if(id == 0) { //child
            Service(server_socket_fd, client_ip,client_port);
            exit(4);
        }
    }
}

network test

After recompiling the program and running the server, you can monitor the service process through the following monitoring script

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

At the beginning, there is no client connecting to the server. At this time, there is only one service process. The service process is the process of continuously obtaining new connections. After obtaining new connections, the process also creates child processes to provide services for the corresponding clients.

At this time, start a client and let the client connect to the server. At this time, the service process will call the fork function to create a child process, and the child process will provide services for the client.

If another client connects to the server, the service process will create another child process to provide services for this client

The two clients are served by two different execution streams, so the two clients can enjoy the service at the same time, the data sent to the server can be output on the server, and the data of the two clients will be processed by the server. respond

When the client exits one after another, the sub-processes that provide services for it on the server side will also exit, but in any case, there will be at least one service process on the server side, and the task of this service process is to continuously obtain new connections

2.3 The grandson process provides services

  • Grandpa process: the process that calls the accept function on the server side to obtain the client connection request
  • Dad process: the process created by the grandfather process calling the fork function
  • Grandchild process: the process created by the father process calling the fork function, which calls the Service function to provide services for the client

The father process exits immediately after creating the grandson process. At this time, the service process (grandfather process) calls the wait/waitpid function to wait for the father process to wait for success immediately. At this time, the grandson process becomes an orphan process and is adopted by the No. 1 process. After that, the service process Can continue to call the accept function to obtain connection requests from other clients. There is no need to deal with grandchildren, whose resources are released by the system

Close the corresponding file descriptor

After the service process (grandfather process) calls the accept function to obtain a new connection, it will let the grandson process provide services for the server corresponding to the connection. At this time, the service process has inherited the file descriptor table to the father process, and the father process will Call the fork function to create a grandchild process, and then inherit the file descriptor table to the grandchild process

After the parent and child processes are created, their respective file descriptor tables are independent and will not affect each other. Therefore, after the service process calls the fork function, the service process does not need to care about the file descriptor obtained from the accept function just now. At this time, the service process can call the close function to close the file descriptor

For the father process and the grandson process, there is no need to care about the listening socket inherited from the service process (grandfather process), so the service process can turn off the listening socket

  • For the service process, after calling the fork function, the file descriptor obtained from the accept function must be closed. Because the service process will continuously call the accept function to obtain new file descriptors (service sockets), if the service process does not close unused file descriptors in time, the number of file descriptors available in the service process will eventually become less and less
  • For grandchildren processes, it is still recommended to close the listening socket inherited from the service process. In fact, even if the listening socket is not closed, it will only cause this file descriptor to leak in the end, but it is still recommended to close it. Because the grandson process may make some misoperations on the listening socket when providing services, this will affect the data in the listening socket
  • In actual coding, close the listening socket before the parent process forks, and naturally there is no listening socket in the file descriptor table inherited by the grandson process
void TcpServer::StartUp()
{
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        pid_t id = fork();
        if(id == 0) { //爸爸进程
            close(_socket_listen_fd);//关闭监听套接字
            if(fork() > 0) exit(4);//服务进程子进程直接退出
            //孙子进程处理
            Service(server_socket_fd, client_ip,client_port);
            exit(5);
        }
        close(server_socket_fd);//服务进程关闭连接客户端时获取的文件描述符
        waitpid(id, nullptr, WNOHANG);//等待爸爸进程,立即成功
    }
}

network test

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

At this time, no client is connected to the server, only one service process is monitored, and the service process is waiting for the connection request from the client 

At this time, start a client and let the client connect to the server. At this time, the service process will create a father process, and the father process will create a grandson process. After that, the father process will exit immediately, and the grandson process will provide services for the client. . So only two service processes are seen, one of which is the service process used to obtain the connection at the beginning, and the other is the grandson process, which provides services for the current client, and its PPID is 1, indicating that this is an orphan process

After starting the second client to connect to the server, an orphan process will be created to serve the client

The two clients are served by two different orphan processes, so they can enjoy the service at the same time. You can see that the data sent by the two clients to the server can be output on the server, and the server will also respond to the Both clients respond with data

When all the clients exit, the corresponding orphan processes that provide services for the clients will also exit. At this time, these orphan processes will be recycled by the system, and finally the service process that obtains the connection is left

3. Multi-threaded version of TCP network program

The cost of creating a process is very high. When creating a process, it is necessary to create structures such as the process control block (task_struct), process address space (mm_struct), and page table corresponding to the process. The cost of creating a thread is much smaller than the cost of creating a process, because the thread essentially runs in the address space of the process, and the created thread will share most of the resources of the process, so it is best when implementing a server with multiple execution streams Implementation using multithreading

After the service process calls the accept function to obtain a new connection, it can create a thread and let the thread provide services for the corresponding client

After the main thread creates a new thread, it also needs to wait for the new thread to exit to recycle resources, otherwise it will also cause resource waste. But for threads, if you don't want the main thread to wait for the new thread to exit, you can let the created new thread call the pthread_detach function to detach the thread. When the thread exits, the system will automatically reclaim the resources corresponding to the thread. At this point, the main thread can continue to call the accept function to obtain a new connection, and let the new thread serve the corresponding client

Each thread shares the same file descriptor table

The file descriptor table maintains the correspondence between processes and files, so a process corresponds to a file descriptor table. The new thread created by the main thread still belongs to this process, so when creating a thread, no independent file descriptor table is created for the thread, and all threads see the same file descriptor table

When the main thread calls the accept function to obtain a file descriptor, the new thread can directly access the file descriptor

Although the new thread can directly access the file descriptor from the main thread accept, but at this time the new thread does not know which file descriptor the client it serves corresponds to, so the main thread needs to tell the new thread to correspond to The file descriptor that should be accessed, which tells each new thread which socket it should operate on when servicing the client

parameter structure

The actual new thread calls the Service function when providing services for the client, and when calling the Service function, three parameters need to be passed in, which are the corresponding socket, IP address and port number of the client. Therefore, when the main thread creates a new thread, it needs to pass in three parameters to the new thread, but when actually calling the pthread_create function to create a new thread, only one parameter of type void* can be passed in

At this time, a parameter structure ThreadDate can be designed. These three parameters can be stored in the ThreadDate structure. When the main thread creates a new thread, a ThreadDate object can be defined, and the corresponding socket, IP address and port number of the client can be set. Enter this ThreadDate object, and then pass in the address of the Param object as the parameter of the new thread execution routine

At this time, the new thread will force the parameter of void* type to Param* type in the execution routine, and then can get the corresponding socket, IP address and port number of the client, and then call the Service function for the corresponding client. end service

class ThreadDate
{
public:
    ThreadDate(int fd, string ip,uint16_t port):_server_socket_fd(fd),_client_ip(ip),_client_port(port) {}
    ~ThreadDate() {}
public:
    int _server_socket_fd;//accept获取连接得到文件描述符,用于服务
    string _client_ip;
    uint16_t _client_port;
};

Problem with file descriptor closing

All threads see the same file descriptor table, so when a thread wants to perform some operation on the file descriptor table, not only the current thread but also other threads must be considered.

  • For the file descriptor accepted by the main thread, the main thread cannot close it, and the closing operation of the file descriptor should be performed by a new thread. Because the new thread provides services for the client, the file descriptor can only be closed when the service provided by the new thread for the client ends
  • For the listening socket, although the new thread created does not need to care about the listening socket, the new thread cannot close the file descriptor corresponding to the listening socket, otherwise the main thread cannot obtain new connections from the listening socket.

Service function is defined as a static member function

When the pthread_create function is called to create a thread, the execution routine of the new thread is a function whose parameter is void* and the return value is void*. To define this execution routine in a class, it needs to be defined as a static member function, otherwise the first parameter of this execution routine is the hidden this pointer

The Service function is called in the execution routine of the thread. Since the execution routine is a static member function, the static member function cannot call the non-static member function. Therefore, the Service function needs to be defined as a static member function. It happens that the operations performed inside the Service function are not involved. The modification of data in the class, so add a static directly in front of the Service function

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port) {}
    void InitServer();
    void StartUp();
    static void Service(int, string, uint16_t);
    ~TcpServer();
private:
    int _socket_listen_fd;
    uint16_t _server_port;
};
void TcpServer::StartUp()
{
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        ThreadDate* ptr = new ThreadDate(server_socket_fd, client_ip, client_port);
        pthread_t thread_id;
        pthread_create(&thread_id, nullptr, HandlerClient, (void*)ptr);
        /*应将ThreadDate数据开辟在堆区,若开辟在主线程栈区,主线程循环accept并处理客户端请求时,会修改TheadDate内数据*/
    }
}

void* TcpServer::HandlerClient(void* args)
{
    pthread_detach(pthread_self());//线程分离,资源由系统回收
    ThreadDate* ptr = (ThreadDate*)args;
    Service(ptr->_server_socket_fd, ptr->_client_ip, ptr->_client_port);
    delete ptr;
    return nullptr;
}

network test

The ps -axj command is no longer used for monitoring, but the ps -aL command

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

Start the server. Through monitoring, it is found that there is only one service thread (main thread) at this time, and it is now waiting for the connection request from the client.

When a client connects to the server, the main thread will build a parameter structure for the client, then create a new thread, and pass the address of the parameter structure to the new thread as a parameter. The new thread can extract the corresponding parameters from this parameter structure, and then call the Service function to provide services for the client, so two threads are displayed in the monitoring

When the second client sends a connection request, the main thread will perform the same operation, and finally create a new thread to provide services for the client. At this time, there are three threads

Since the two clients are served by two different execution streams, the two clients can enjoy the services provided by the server at the same time, and the messages sent to the server can be printed on the server, and the two clients Both ends can receive the echo data from the server

At this time, no matter how many clients send connection requests, a corresponding number of new threads will be created on the server to provide services for the corresponding clients, and when the clients exit one by one, the new threads that provide services for them will also be closed. Exit one after another, and finally only the main thread is left waiting for the arrival of new connections

4. Thread pool version TCP network program

Problems with pure multithreading

  • Whenever a new connection arrives, the main thread of the server will re-create a new thread to provide services for the client, and when the service ends, the new thread will be destroyed. This is not only cumbersome, but also inefficient. The server creates a corresponding service thread whenever a connection arrives.
  • If there are a large number of client connection requests, the server should create a corresponding service thread for each client. The more threads in the computer, the greater the pressure on the CPU, because the CPU has to constantly switch back and forth between these threads. At this time, when the CPU schedules threads, the cost of switching between threads will become very high. In addition, once there are too many threads, the period for each thread to be scheduled again becomes longer, and the thread provides services for the client, the thread is scheduled for a longer period, and the client experience will also deteriorate

solution

  • You can pre-create a batch of threads on the server side, and let these threads provide services for the client when a client requests a connection. At this time, the thread will provide services for the client as soon as the client comes, instead of creating it when the client comes. Corresponding service thread
  • After a thread has provided services for the client, do not let the thread exit, but let the thread continue to provide services for the next client. If there is no client connection request, you can let the thread enter the dormant state first. Wake up the thread when a client connection arrives
  • The number of threads created by the server should not be too large, and the pressure on the CPU will not be too great. In addition, if a client connection arrives, but this batch of threads is providing services to other clients at this time, the server should not create any more threads at this time, but should queue up the new connection request in the full connection queue , after there are idle threads in the batch of threads on the server side, then obtain the connection request and provide services for it

Introduce thread pool

To solve the problem, it is necessary to introduce a thread pool on the server side. The existence of the thread pool is to avoid the cost of creating and destroying threads when processing short-term tasks, and to ensure that the kernel is fully utilized to prevent excessive scheduling (scheduling cycle is too long)

There is a task queue in the thread pool. When a new task arrives, the task can be pushed to the thread pool. By default, 10 threads are created in the thread pool. These threads continuously check whether there are tasks in the task queue. If there is a task, take out the task, and then call the Run function corresponding to the task to process the task. If there is no task in the thread pool, the current thread will enter the dormant state

The code of the thread pool is directly connected to the current TCP server. The following will only explain the method of thread pool access. If you have any doubts about the implementation of the thread pool, you can read the blogger's blog "Understanding and Implementation of Thread Pool"

Understanding and implementing thread pool https://blog.csdn.net/GG_Bruse/article/details/129616793

Service class adds thread pool members

The server introduces a thread pool, so a pointer member pointing to the thread pool needs to be added in the service class:

  • When constructing a thread pool object, you can specify the number of threads in the thread pool. At this time, the default number of threads is 10.
  • When constructing a thread pool, several threads in the thread pool will be created, and after these threads are created, the task queue will be continuously detected, and tasks will be taken out from the task queue for processing
  • When the service process calls the accept function to obtain a connection request, it will construct a task according to the client's socket, IP address and port number, and then call the Push interface provided by the thread pool to put the task into the task queue

In fact, it is a producer-consumer model, in which the service process acts as the producer of the task, and several threads in the back-end thread pool continuously obtain tasks from the task queue for processing, and assume the role of the consumer. The trading place for buyers and consumers is the task queue in the thread pool

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port),_thread_pool(ThreadPool<Task>::GetThreadPool()) {}
    void InitServer();
    void StartUp();
    static void* HandlerClient(void*);
    static void Service(int, string, uint16_t);
    ~TcpServer();
private:
    int _socket_listen_fd;
    uint16_t _server_port;
    unique_ptr<ThreadPool<Task>> _thread_pool;
};
void TcpServer::StartUp()
{
    _thread_pool->Run();//启动线程池
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //构造任务并推送到任务队列中
        Task task(server_socket_fd, client_ip, client_port, Service);
        _thread_pool->PushTask(task);
    }
}

design task class

The task class needs to include the socket, IP address, and port number corresponding to the accept client, which indicates which client the task provides services for, and which socket is the corresponding operation

The task class needs to contain a functor method. When the thread in the thread pool gets the task, it will directly call the functor to process the task. The actual method of processing this task is the Service function in the service class. The server is Provide services to clients by calling the Service function

typedef void(*fun_t)(int, std::string, uint16_t);
class Task
{
public:
	Task() {}
	Task(int sock, std::string client_ip, int client_port, fun_t handler) : _server_socket_fd(sock)
		, _client_ip(client_ip), _client_port(client_port), _handler(handler) {}
	//任务处理函数
	void operator()(const std::string& name) {
        _handler(_server_socket_fd, _client_ip, _client_port);
	}
private:
	int _server_socket_fd;
	std::string _client_ip;
	uint16_t _client_port;
    fun_t _handler;
};

In fact, the server can be used to handle different tasks. Currently, the server is only echoing strings, but how to actually handle this task is completely determined by the _handler member of the task class.

If you want the server to handle other tasks, you only need to modify the overloaded function of (), and the codes of server initialization, server startup and thread pool do not need to be changed. This is called combining communication functions and business logic. Decoupling in software

network test

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

After running the server, even if no client sends a connection request, there are already 11 threads on the server at this time, one of which is a service thread that receives a new connection, and the remaining 5 threads are in the thread pool for the client. service thread 

When the client connects to the server, the main thread of the server will obtain the client's connection request, encapsulate it into a task object and put it into the task queue. At this time, one of the 10 threads in the thread pool will start from the task The task is obtained from the queue, and the processing function of the task is executed to provide services for the client

When the second client initiates a connection request, the server will also encapsulate it as a task class and put it in the task queue, and then the threads in the thread pool will obtain the task from the task queue for processing, which is also different at this time The execution flow provides services for these two clients, so these two clients can enjoy the service at the same time

No matter how many clients send requests, there will only be 10 threads in the thread pool to provide services on the server side. The number of threads in the thread pool will not increase with the increase of client connections. Will not exit due to client exit

Thread pool version TCP program https://github.com/GG-Bruse/BaoLinux/tree/master/code/socket/TCP/TCP_ThreadPool

5. Address conversion function

5.1 String IP to integer IP

inet_aton function

int inet_aton(const char *cp, struct in_addr *inp);
  • cp: the string IP to be converted.
  • inp: converted integer IP, output parameter

Return value: Returns a non-zero value if the conversion is successful, or returns zero if the input address is incorrect

inet_addr function

in_addr_t inet_addr(const char *cp);

Parameter cp: the string IP to be converted

Return value: If the input address is valid, it will return the converted integer IP; if it is invalid, it will return INADDR_NONE (-1)

inet_pton function

int inet_pton(int af, const char *src, void *dst);
  • af parameter: protocol family
  • src parameter: the string IP to be converted
  • dst parameter: converted integer IP, output parameter

Return value description:

  • Returns 1 if the conversion is successful
  • If the input string IP is invalid, return 0
  • If the input protocol family af is invalid, return -1 and set errno to EAFNOSUPPORT

5.2 Integer IP to string IP

inet_ntoa function

char *inet_ntoa(struct in_addr in);

Parameter in: the integer IP to be converted

Return value: return the converted string IP

inet_ntop function

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af parameter: protocol family
  • src parameter: the integer IP to be converted
  • dst parameter: converted string IP, output parameter
  • size parameter: used to indicate the number of bytes available in dst

Return value: If the conversion is successful, it returns a non-null pointer pointing to dst; if the conversion fails, it returns NULL.

Notice 

  • The two most commonly used conversion functions are inet_addr and inet_ntoa, because these two functions are simple enough. The parameters of these two functions are the string IP or integer IP that needs to be converted, and the return value of these two functions is the corresponding integer IP and string IP
  • Among them, the inet_pton and inet_ntop functions can not only convert IPv4 in_addr, but also convert IPv6 in6_addr, so the corresponding parameter type in these two functions is void*
  • The conversion functions are all to meet certain printing scenarios or to do some data analysis, such as data analysis in network security

5.3 inet_ntoa function problem

The inet_ntoa function can convert a 4-byte integer IP into a string IP, and the converted string IP returned by this function is stored in a static storage area and does not need to be released manually by the caller. If the inet_ntoa function is called multiple times, there will be a problem of data overwriting

The inet_ntoa function only applies for an area in the static storage area, causing the result of the second conversion of the inet_ntoa function to overwrite the result of the first conversion

If you want to call the inet_ntoa function multiple times, you must save the conversion result of inet_ntoa in time

inet_ntoa function in concurrent scenarios

The inet_ntoa function only applies for an area in the static storage area to store the converted string IP. In the thread scenario, this area is called a critical area. It is inevitable for multiple threads to access the critical area at the same time without locking. Exceptions will occur. And in APUE, it is also clearly stated that inet_ntoa is not a thread-safe function

#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>

void* Func1(void* arg)
{
	struct sockaddr_in* p = (struct sockaddr_in*)arg;
	while (1){
		char* ptr1 = inet_ntoa(p->sin_addr);
		std::cout << "ptr1: " << ptr1 << std::endl;
		sleep(1);
	}
}
void* Func2(void* arg)
{
	struct sockaddr_in* p = (struct sockaddr_in*)arg;
	while (1){
		char* ptr2 = inet_ntoa(p->sin_addr);
		std::cout << "ptr2: " << ptr2 << std::endl;
        sleep(1);
	}
}
int main()
{
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	
	pthread_t tid1 = 0;
	pthread_create(&tid1, nullptr, Func1, &addr1);
    sleep(1);
	pthread_t tid2 = 0;
	pthread_create(&tid2, nullptr, Func2, &addr2);
	
	pthread_join(tid1, nullptr);
	pthread_join(tid2, nullptr);
	return 0;
}

However, when actually testing on centos7, there is no problem in calling the inet_ntoa function in a multi-threaded scenario. It may be that a mutex is added to the internal implementation of the function, which is also related to the design of the interface itself. 

In a multi-threaded environment, it is more recommended to use the inet_ntop function for conversion, because the caller provides a buffer to save the conversion result, which can avoid thread safety issues

Guess you like

Origin blog.csdn.net/GG_Bruse/article/details/129803241
Recommended