[Network programming] demo version TCP network server implementation

1. Introduction

The difference between UDP and TCP:

There are several characteristics for the TCP protocol:

1️⃣ Transport layer protocol
2️⃣ Connection (connection must be established before formal communication)
3️⃣ Reliable transmission (help us do reliable transmission internally)
4️⃣ Oriented to byte stream

There are several characteristics for the UDP protocol:

1️⃣ Transport layer protocol
2️⃣ Connectionless
3️⃣ Unreliable transmission
4️⃣ Datagram-oriented

You can see that TCP vs. UDP will establish a link .

There is no difference between other interfaces and UDP: [Network programming] demo version UDP network server implementation

Second, the server implementation

2.1 Create a socket socket

Before communicating, the network card file must be opened.

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

RETURN VALUE
On success, a file descriptor for the new socket is returned.  
On error, -1 is returned, and errno is set appropriately.

The function of this function is to open a file and associate the file with the network card.

Parameter introduction:

domain: A field that identifies the communication type of this socket (network or local).
insert image description here
Just focus on the above two classes, the first one AF_UNIXmeans local communication, and AF_INETthe other means network communication.
type: The type of service provided by the socket.
insert image description here
In this chapter we talked about TCP, so use it SOCK_STREAM.
protocol: The protocol you want to use, the default is 0, because the previous two parameters determine whether it is TCP or UDP protocol.

return value:

Returns the open file descriptor (pointing to the network card file) if successful, returns -1 if failed.

And from here we think of the file operations in the system. In the future, various operations will pass through this file descriptor, so a member variable is needed to represent the file descriptor in the server class.

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"

class TCPServer
{
    
    
static const uint16_t gport = 8080;
public:
    TCPServer(cosnt uint16_t& port = gport)
        : _sock(-1)
        , _port(port)
    {
    
    }

    void InitServer()
    {
    
    
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sockfd == -1)
        {
    
    
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
        std::cout << "create socket success" << std::endl;
    }

    void start()
    {
    
    }
private:
    int _sock;
    uint16_t _port;
};

2.2 binding bind

#include <sys/socket.h>

int bind(int socket, const struct sockaddr *address,
       socklen_t address_len);

RETURN VALUE
Upon successful completion, bind() shall return 0; 
otherwise, -1 shall be returned and errno set to indicate the error.

Parameter introduction:

socket: The return value of creating a socket.
address: General structure ( [Network Programming] socket sockets are introduced in detail).
address_len: The length of the incoming structure.

So we need to define a sockaddr_instructure to fill in the data first, and then pass it in.
insert image description here
Then it is the same as UDP, first initialize the structure, and then process the IP and port.
It should be noted that the IP should be bound to any IP, that is INADDR_ANY.
As for why the previous chapter [Network Programming] demo version UDP network server implementation has been explained in detail.

void InitServer()
{
    
    
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if(_sockfd == -1)
    {
    
    
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    std::cout << "create socket success" << std::endl;
    struct sockaddr_in si;
    // 初始化结构体
    bzero(&si, sizeof si);
    si.sin_family = AF_INET;
    si.sin_port = htons(_port);// 主机转网络序列
    si.sin_addr.s_addr = INADDR_ANY;
    if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
    {
    
    
        std::cout << "bind socket error" << std::endl;
        exit(1);
    }
    std::cout << "bind socket success" << std::endl;
}

2.3 Set the monitoring status listen

The difference between TCP and UDP is reflected here.
Set the state of the socket socket to the listen state. Only in this way can we always get new links and receive new link requests.

For example:
if we have a problem with shopping, we will go to the customer service. If the customer service is not available, we will not be able to reply. Therefore, it is stipulated that the customer service must always receive reply messages when working. The state of this customer service is called the monitoring state .

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

RETURN VALUE
On success, zero is returned.  
On error, -1 is returned, and errno is set appropriately.

Regarding the second parameter backlog, it will be introduced later when talking about the TCP protocol, and it will be used directly at present.

static const int gbacklog = 10;
void InitServer()
{
    
    
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if(_sock == -1)
    {
    
    
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    std::cout << "create socket success" << std::endl;
    struct sockaddr_in si;
    // 初始化结构体
    bzero(&si, sizeof si);
    si.sin_family = AF_INET;
    si.sin_port = htons(_port);// 主机转网络序列
    si.sin_addr.s_addr = INADDR_ANY;
    if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
    {
    
    
        std::cout << "bind socket error" << std::endl;
        exit(1);
    }
    std::cout << "bind socket success" << std::endl;
    // 设置监听状态
    if(listen(_sock, gbacklog) < 0)
    {
    
    
        std::cout << "listen socket error" << std::endl;
        exit(1);
    }
    std::cout << "listen socket success" << std::endl;
}

2.4 Get new link accept

The above initialization is completed, and now the server is to be run, and TCP cannot directly send data, because it is link-oriented, and the link must be established first .

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket.  
On error, -1 is returned, and errno is set appropriately.

Parameter introduction:

sockfdThe file descriptor, which finds the socket
addrinput and output parameters, is a structure used to obtain client information.
addrlenInput and output parameters, the size of the structure passed by the client.

return value:

Returns a file descriptor
on success , -1 on failure

And we know that sockfd is originally a file descriptor, so what is the returned file descriptor?
for example:

When we go to eat, we will find that there will be someone at the door of each store to attract customers. After this person leads us into the store, he will continue to stand at the door to continue to attract customers, and we will have waiters inside to entertain us. We provide services.

The sockfd is the sockfd here, and the waiter is the file descriptor of the return value.
It means that the function of sockfd is to obtain the link from the bottom layer, and the function of the return value is to communicate with the client.

From here, we know that the socket in the member variable _sockis not the socket used for communication, but the socket used to obtain the link. For the convenience of observation, we can put all _sock换into _listensock.

void start()
{
    
    
    while(1)
    {
    
    
        // 获取新链接
        struct sockaddr_in si;
        socklen_t len = sizeof si;
        int sock = accept(_listensock, (struct sockaddr*)&si, &len);
        if(sock < 0)
        {
    
    
            // 获取链接失败无影响,继续获取即可
            std::cout << "accept error, continue" << std::endl;
            continue;
        }
        std::cout << "accept a new link success" << std::endl;
        std::cout << "sock: " << sock << std::endl;
    }
}

2.5 Get information and return information (file operation)

The socket sock used for communication is obtained above, and because TCP communication is byte-oriented, all subsequent communication uses file operations (IO), because files are also byte-oriented .

We can encapsulate a function for IO operations.

void ServerIO(int sock)
{
    
    
    char buf[1024];
    // 接收消息       
    while(1)
    {
    
    
        ssize_t n = read(sock, buf, sizeof buf - 1);
        if(n > 0)
        {
    
    
            buf[n] = '\0';
            std::cout << "read a message: " << buf << std::endl;
            // 把消息发送回去
            std::string outbuf;
            outbuf += "Server[echo]#";
            outbuf += buf;
            write(sock, outbuf.c_str(), outbuf.size());
        }
        else if(n == 0)
        {
    
    
            // 代表客户端退出
            std::cout << "Client quit" << std::endl;
            break;
        }
    }
}

Remember to close the file descriptor sock after the IO , otherwise the available descriptors will become less and less.
insert image description here
Verify that the discovery can run
insert image description hereinsert image description here

3. Client implementation

3.1 Create a socket socket

Socket can be regarded as an endpoint in the communication connection between two programs. One program writes a piece of information into the Socket, and the Socket sends this piece of information to another Socket, so that this piece of information can be transmitted to other programs.
So the client needs a socket too.

void initClient()
{
    
    
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if(_sock < 0)
    {
    
    
        std::cout << "create socket error" << std::endl;
        exit(1);
    }
    std::cout << "create socket success" << std::endl;
}

3.2 Binding problem

Binding is the same as in the previous chapter, but the binding cannot be displayed .

3.3 Initiate the link connect

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
            
RETURN VALUE
If the connection or binding succeeds, zero is returned.  
On error, -1 is returned, and errno is set appropriately.

Parameter Description:

Here addrand addrlenfilled in is the server information.

In UDP communication, the client will automatically bind IP and port when sending to, and TCP is bound when connecting.

void start()
{
    
    
    struct sockaddr_in si;
    bzero(&si, sizeof si);
    si.sin_family = AF_INET;
    si.sin_port = htons(_serverport);
    si.sin_addr.s_addr = inet_addr(_serverip.c_str());
    if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0)
    {
    
    
        std::cout << "connect socket error" << std::endl;
    }
    else
    {
    
    
        std::string msg;
        while(1)
        {
    
    
            std::cout << "Please Enter#";
            std::getline(std::cin, msg);
            write(_sock, msg.c_str(), msg.size());
            // 从服务端读取数据
            char buf[1024];
            int n = read(_sock, buf, sizeof buf - 1);
            if(n > 0)
            {
    
    
                buf[n] = '\0';
                std::cout << buf << std::endl;
            }
            else
            {
    
    
                break;
            }
        }
    }
}

Finally, the file descriptor must be closed when it is destructed.

~TCPClient()
{
    
    
    if(_sock >= 0)
    {
    
    
        close(_sock);
    }  
}

Server:
insert image description here
Client:
insert image description here
But if we are using a client connection at this time, we will find that we cannot communicate unless the first client exits.
insert image description here
This is because it will enter an infinite loop in ServerIo after obtaining a new link. As long as the client does not exit, it will always provide services to the client.

So how to ensure that multiple clients are parallelized?

3.4 Client parallelism

3.4.1 Multi-process version

Because after fork, the child process will copy the file descriptor of the parent process.
Note here that the child process does not need _listensocka file descriptor, so it is best to close it.

pid_t id = fork();
if(id == 0)// child
{
    
    
    close(_listensock);
    ServerIO(sock);
    close(sock);
    exit(1);
}

What should the parent process do next? Is it waiting?
If the parent process waits, it will lead to the above situation, and the child process will wait without exiting the parent process.

When the child process exits, it will send a SIGCHLD, signal No. 17, to the parent process. So one solution is to use signala function, set the waitpid parameter to -1 (wait for any process) in the callback function, and then it can be recycled.

Now instead of this approach, we can write:

pid_t id = fork();
if(id == 0)// child
{
    
    
    close(_listensock);
    if(fork() > 0) exit(1);
    ServerIO(sock);
    close(sock);
    exit(1);
}
// father
pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
    
    
    std::cout << "wait success" << ret << std::endl;
}

The meaning here is to create a grandson process, the parent process exits directly, and let the grandson process execute ServerIO. At this time, the grandson process will be adopted by the operating system without our management, and the parent process exits, and the outside parent process also waits for success.

Result demonstration:
Client:
insert image description here
insert image description here
Server:
insert image description here
In fact, the following waits do not need to wait, because the default processing method of SIGCHLD signal is to ignore.
insert image description here
insert image description here
Here we see that the client has exited but the file descriptor has not been recycled.
The reason here is that we only close the file descriptor of the child process, not the parent process:
insert image description here

3.4.2 Multithreaded version

struct ThreadData
{
    
    
    TCPServer* _self;
    int _sock;
};

void start()
{
    
    
    while(1)
    {
    
    
        // 获取新链接
        struct sockaddr_in si;
        socklen_t len = sizeof si;
        int sock = accept(_listensock, (struct sockaddr*)&si, &len);
        if(sock < 0)
        {
    
    
            // 获取链接失败无影响,继续获取即可
            std::cout << "accept error, continue" << std::endl;
            continue;
        }
        std::cout << "accept a new link success" << std::endl;
        std::cout << "sock: " << sock << std::endl;
        // 多线程
        pthread_t tid;
        ThreadData* td = new ThreadData({
    
    this, sock});
        pthread_create(&tid, nullptr, thread_start, td);
    }
}

static void* thread_start(void* args)
{
    
    
    // 线程分离
    pthread_detach(pthread_self());
    ThreadData* tp = static_cast<ThreadData*>(args);
    tp->_self->ServerIO(tp->_sock);
    close(tp->_sock);
    delete tp;
}

3.4.3 Thread pool version

We wrote earlier that the thread pool [linux] implements the thread pool based on the singleton mode , and it can be used directly here.
Here we need to modify the code, because ServerIO may not belong to a class, so we can put ServerIO in the task Task.hpp.

// Task.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

void ServerIO(int sock)
{
    
    
    char buf[1024];
    // 接收消息       
    while(1)
    {
    
    
        ssize_t n = read(sock, buf, sizeof buf - 1);
        if(n > 0)
        {
    
    
            buf[n] = '\0';
            std::cout << "read a message: " << buf << std::endl;
            // 把消息发送回去
            std::string outbuf;
            outbuf += "Server[echo]#";
            outbuf += buf;
            write(sock, outbuf.c_str(), outbuf.size());
        }
        else if(n == 0)
        {
    
    
            // 代表客户端退出
            std::cout << "Client quit" << std::endl;
            break;
        }
    }
}

class Task
{
    
    
    typedef std::function<void(int)> func_t;
public:
    Task()
    {
    
    }

    Task(int sock, func_t func)
        : _sock(sock)
        , _func(func)
    {
    
    }

    void operator()()
    {
    
    
        _func(_sock);
    }

    std::string tostringTask()
    {
    
    
        char buf[64];
        snprintf(buf, sizeof buf, "%d %c %d = ?", _x, _op, _y);
        return buf;
    }
private:
    int _sock;
    func_t _func;
};

// ThreadPool.hpp
#pragma once

#include <vector>
#include <queue>
#include <mutex>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"

using std::cout;
using std::endl;

const int N = 5;

template <class T>
class ThreadPool;

template <class T>
struct ThreadData
{
    
    
    ThreadPool<T>* _tp;
    std::string _name;
    ThreadData(ThreadPool<T>* tp, const std::string& name)
        : _tp(tp)
        , _name(name)
    {
    
    }
};

template <class T>
class ThreadPool
{
    
    
private:
    static void* handlerTask(void* args)
    {
    
    
        ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
    
    
            tdp->_tp->lockqueue();
            while(tdp->_tp->isqueueempty())
            {
    
    
                tdp->_tp->threadwait();
            }
            T t = tdp->_tp->pop();
            tdp->_tp->unlockqueue();
            t();
        }
        delete tdp;
    }

    void lockqueue() volatile
    {
    
    
        pthread_mutex_lock(&_mutex);
    }

    void unlockqueue() volatile
    {
    
    
        pthread_mutex_unlock(&_mutex);
    }

    bool isqueueempty() volatile
    {
    
    
        return _tasks.empty();
    }

    void threadwait() volatile
    {
    
     
        pthread_cond_wait(&_cond, &_mutex);
    }

    T pop() volatile
    {
    
    
        T res = _tasks.front();
        _tasks.pop();
        return res;
    }

    ThreadPool(int num = 5)
        : _num(num)
    {
    
    
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 创建线程
        for(int i = 0; i < _num; i++)
        {
    
    
            _threads.push_back(new Thread());
        }
    }

    ThreadPool(const ThreadPool<T>& ) = delete;
    ThreadPool<T> operator=(const ThreadPool<T>&) = delete;

public:

    void start() volatile
    {
    
    
        for(auto& t : _threads)
        {
    
    
            ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
            t->start(handlerTask, td);
        }
    }

    void push(const T& in) volatile
    {
    
    
        LockAuto lock(&_mutex);
        _tasks.push(in);
        // 唤醒池中的一个线程
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
    
    
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto & e : _threads)
        {
    
    
            delete e;
        }
    }

    volatile static ThreadPool<T>* GetSingle()
    {
    
    
        if(_tp == nullptr)
        {
    
    
            _singlelock.lock();
            if(_tp == nullptr)
            {
    
    
                _tp = new ThreadPool<T>();
            }
            _singlelock.unlock();
        }
        return _tp;
    }

private:
    int _num;// 线程数量
    std::vector<Thread*> _threads;
    std::queue<T> _tasks;// 任务队列
    pthread_mutex_t _mutex;// 保护任务队列
    pthread_cond_t _cond;
    volatile static ThreadPool<T>* _tp;
    static std::mutex _singlelock;
};

template <class T>
volatile ThreadPool<T>* ThreadPool<T>::_tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlelock;

// TCPServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <cstdlib>
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include "log.hpp"

class TCPServer;
struct ThreadData
{
    
    
    TCPServer* _self;
    int _sock;
};

class TCPServer
{
    
    
static const uint16_t gport = 8080;
static const int gbacklog = 10;
public:
    TCPServer(const uint16_t& port = gport)
        : _listensock(-1)
        , _port(port)
    {
    
    }

    void InitServer()
    {
    
    
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock == -1)
        {
    
    
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
        std::cout << "create socket success" << std::endl;
        struct sockaddr_in si;
        // 初始化结构体
        bzero(&si, sizeof si);
        si.sin_family = AF_INET;
        si.sin_port = htons(_port);// 主机转网络序列
        si.sin_addr.s_addr = INADDR_ANY;
        if(bind(_listensock, (struct sockaddr*)&si, sizeof si) < 0)
        {
    
    
            std::cout << "bind socket error" << std::endl;
            exit(1);
        }
        std::cout << "bind socket success" << std::endl;
        // 设置监听状态
        if(listen(_listensock, gbacklog) < 0)
        {
    
    
            std::cout << "listen socket error" << std::endl;
            exit(1);
        }
        std::cout << "listen socket success" << std::endl;
    }

    void start()
    {
    
    
        // 线程池初始化
        ThreadPool<Task>::GetSingle()->start();
        while(1)
        {
    
    
            // 获取新链接
            struct sockaddr_in si;
            socklen_t len = sizeof si;
            int sock = accept(_listensock, (struct sockaddr*)&si, &len);
            if(sock < 0)
            {
    
    
                // 获取链接失败无影响,继续获取即可
                std::cout << "accept error, continue" << std::endl;
                continue;
            }
            std::cout << "accept a new link success" << std::endl;
            std::cout << "sock: " << sock << std::endl;
            // 线程池
            ThreadPool<Task>::GetSingle()->push(Task(sock, ServerIO));


            // 多线程
            // pthread_t tid;
            // ThreadData* td = new ThreadData({this, sock});
            // pthread_create(&tid, nullptr, thread_start, td);
            
            // 多进程
            // pid_t id = fork();
            // if(id == 0)// child
            // {
    
    
            //     close(_listensock);
            //     if(fork() > 0) exit(1);
            //     ServerIO(sock);
            //     close(sock);
            //     exit(1);
            // }
            // close(sock);
            // father
            // pid_t ret = waitpid(id, nullptr, 0);
            // if(ret > 0)
            // {
    
    
            //     std::cout << "wait success " << ret << std::endl;
            // }


            // ServerIO(sock);
            // // 关闭使用完的文件描述符
            // close(sock);
        }
    }

    static void* thread_start(void* args)
    {
    
    
        // 线程分离
        pthread_detach(pthread_self());
        ThreadData* tp = static_cast<ThreadData*>(args);
        tp->_self->ServerIO(tp->_sock);
        close(tp->_sock);
        delete tp;
    }

private:
    int _listensock;
    uint16_t _port;
};

Four. Summary

Compared with the UDP server, the TCP server has more operations for obtaining new connections and monitoring, and because TCP is oriented to byte streams, receiving and sending data are all IO operations, that is, file operations.

Guess you like

Origin blog.csdn.net/qq_66314292/article/details/130781612