Network byte order - TCP interface and its implementation as a simple TCP server

Network byte order - TCP interface and its implementation as a simple TCP server

Mori Kata (2)

Implementation of a simple TCP server

  • The difference between TCP and UDP is that the socket needs to be set to the monitoring state, that is, TCP is link-oriented, so the TCP socket needs to be set to the listening state.
void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

socket function prototype

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • domainRepresents a protocol family, commonly used ones are AF_INET(IPv4) and AF_INET6(IPv6).

  • typeIndicates the Socket type, commonly used ones are SOCK_STREAM(TCP) and SOCK_DGRAM(UDP).

  • protocolIt can usually be set to 0 to let the system choose the appropriate protocol based on domainand .type

  • socket() opens a network communication port and, if successful, returns a file descriptor just like open().

  • Applications can use read/write to send and receive data on the network through the socket function just like reading and writing files.

bind function prototype

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfdIs the socket descriptor.

  • addrIs a struct sockaddrstructure containing the IP address and port information to be bound.

  • addrlenis addrthe length of the structure. Because the addr structure can accept the sockaddr structure of multiple protocols, the length of its structure must be passed.

  • bind() returns 0 on success and -1 on failure.

  • The function of bind() is to bind the parameters sockfd and myaddr together so that sockfd, a file descriptor used for network communication, listens to the address and port number described by addr;

listen function prototype

int listen(int sockfd, int backlog);
  • sockfdIs the socket descriptor, which refers to the file descriptor used for network monitoring.
  • backlogIndicates the maximum length of the queue waiting for connections.
  • listen returns 0 on success and -1 on failure.
  • The listen function will put sockfd in the listening state and allow backlog clients to be in the connection waiting state. When more than backlog client connection requests are received, it will be ignored.
  • In fact, the listen function tells the operating system that the specified socket sockfd is in the listening state. The socket begins to wait for other computers to establish connections with it through the network. Once a connection request arrives, the operating system will put the connection request into the connection queue and connect The maximum length of the queue is the backlog. The connection queue is a buffer that stores connection requests. If the queue is full, new connection requests will be rejected. That is, when a socket is in the listening state, it does not directly handle data transmission, but waits for other computers to initiate connections.

In general, the initserver function is to first create a socket, then fill in the specified port number and IP, and set the socket to listening state

void start()
{
    
    
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        }

accept function prototype

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: It is a socketsocket descriptor that has been created through the function and is already in the listening state, used to monitor incoming connection requests.

  • addr: is a struct sockaddrpointer to a structure, used to receive the address information of the client of the connection request.

  • addrlen: is a socklen_tpointer to type, used to specify addrthe length of the buffer, and also used to return the size of the actual client address structure.

  • The accept function is to accept incoming connection requests. It will block the execution of the program until a connection request arrives. Once a connection request arrives, a new socket will be created and the file descriptor of this new socket will be returned. This new socket is used to communicate with the client and addrwill addrlenbe filled with the client's address information. .

  • In the server program, the accept function will be used in a loop to accept connection requests from multiple clients.

The function of the start function is to block the acceptance of connection requests sent by the client, allowing the server to establish communication with the client.

tcpclient.cc

#include<iostream>
#include<string>
#include<memory>
#include"tcpclient.hpp"
using namespace std;
using namespace client;
static void Usage(string proc)
{
    
    
    cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;
}
int main(int argc, char* argv[])
{
    
    
    if(argc!=3)
    {
    
    
        Usage(argv[0]);
        exit(1);
    }

string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);

unique_ptr<tcpclient> tc(new tcpclient(serverip,serverport));

tc->initclient();
tc->start();

    return 0;
}

tcpclient.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>
using namespace std;
#define NUM 1024
namespace client
{
    
    

    class tcpclient
{
    
    

public:
tcpclient(const string& ip,const uint16_t& port)
:_sock(-1)
,_port(port)
,_ip(ip)
{
    
    }

void initclient()
{
    
    
//1.创建sockfd
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
    
    
   cerr<<"socket create error"<<endl;
   exit(2);
}
//2.绑定 ip port,不显示绑定,OS自动绑定
}

void start()
{
    
    
struct sockaddr_in ser;
bzero(&ser,sizeof(ser));
socklen_t len=sizeof(ser);
ser.sin_family=AF_INET;
ser.sin_port=htons(_port);
ser.sin_addr.s_addr=inet_addr(_ip.c_str());
if(connect(_sock,(struct sockaddr *)&ser,len)!=0)
{
    
    
    cerr<<"connect error"<<endl;
}else
{
    
    
    string msg;
    while(true)
    {
    
    
        cout<<"Enter# ";
        getline(cin,msg);
        write(_sock,msg.c_str(),msg.size());
        
        char inbuffer[NUM];
        int n=read(_sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
    
    
            cout<<"server return :"<<inbuffer<<endl;
        }else
        {
    
    
            break;
        }
    }
}
}

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

private:
int _sock;
uint16_t _port;
string _ip;

};
}

tcpserver.cc

#include"tcpserver.hpp"
#include"log.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;

static void Usage(string proc)
{
    
    
    cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}

int main(int argc,char* argv[])
{
    
    
if(argc!=2)
{
    
    
    Usage(argv[0]);
    exit(USAGE_ERR);
}

uint16_t port=atoi(argv[1]);//将字符串转化为整数

unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
ts->start();


return 0;
}

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    
    
    enum
    {
    
    
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver;
    class ThreadData
    {
    
    
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){
    
    }
tcpserver* _this;
int _psock;
    };

class tcpserver
{
    
    
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){
    
    }

void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    
    
     // signal(SIGCHLD, SIG_IGN);
    threadPool<Task>::getthpptr()->run();
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        // serviceIO(sock);//客户端串行版
        // close(sock);

        //多进程版---
        //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
        //因此若接收多个客户端不退出的话文件描述符会越来越少。
//         pid_t id=fork();//创建子进程
//         if(id==0)//子进程进入
//         {
    
    
//             close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
//             if(fork()>0)  exit(0);
// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,
// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信

// //孙子进程
// serviceIO(sock);
// close(sock);
// exit(0);
//         }
//         //父进程
//         pid_t ret=waitpid(id,nullptr,0);
//         if(ret<0)
//         {
    
    
//             cout << "waitsuccess: " << ret << endl;
//         }

//多线程版
// pthread_t pid;
// ThreadData* th=new ThreadData(this,sock);
// pthread_create(&pid,nullptr,start_routine,th);

threadPool<Task>::getthpptr()->push(Task(sock,serviceIO));
    }
}

// static void* start_routine(void* args)
// {
    
    
//     pthread_detach(pthread_self());
//     ThreadData* ret=static_cast<ThreadData*>(args);
//     ret->_this->serviceIO(ret->_psock);
//     close(ret->_psock);
//     delete ret;
//     return nullptr;
// } 

// void serviceIO(int sock)
// {
    
    
//     char inbuffer[NUM];
//     while(true)
//     {
    
    
//         ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
//         if(n>0)
//         {
    
    
//             inbuffer[n]=0;
//             cout<<"recv message: "<<inbuffer<<endl;
//             string outb=inbuffer;
//             string outbuffer=outb+"[server echo]";

//             write(sock,outbuffer.c_str(),outbuffer.size());

//         }
// else
// {
    
    
//     logMessage(NORMAL,"client quit,i quit yep");
//     break;
// }
//     }

// }

~tcpserver(){
    
    }

private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}

1. Single process version: client serial version

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    
    
    enum
    {
    
    
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };

class tcpserver
{
    
    
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){
    
    }

void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    
    
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
         serviceIO(sock);//客户端串行版
         close(sock);
    }
}


void serviceIO(int sock)
{
    
    
    char inbuffer[NUM];
    while(true)
    {
    
    
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
    
    
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    
    
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}

~tcpserver(){
    
    }

private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}

Note: Where is the client blocked when sending data serially to the server? Since it is blocked at the accept function, that is, accept waits for the client to access, which is a blocking wait. After the accept function receives a connection request, subsequent client connection requests need to wait at the accept function. When the previous client exits, the server can successfully accept the connection request sent by the current client and receive the current client's data. That is, the server serially receives and processes the data sent by the client.

2. Multi-process version: client parallel version

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    
    
    enum
    {
    
    
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver
{
    
    
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){
    
    }

void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    
    
     // signal(SIGCHLD, SIG_IGN);
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;

                //多进程版---
        //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
        //因此若接收多个客户端不退出的话文件描述符会越来越少。
        pid_t id=fork();//创建子进程
        if(id==0)//子进程进入
        {
    
    
            close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
            if(fork()>0)  exit(0);
// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,
// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信

// //孙子进程
serviceIO(sock);
close(sock);
exit(0);
        }
        //父进程
         // close(sock);//父进程不使用文件描述符就关闭
        pid_t ret=waitpid(id,nullptr,0);
        if(ret<0)
        {
    
    
            cout << "waitsuccess: " << ret << endl;
        }
    }
}
void serviceIO(int sock)
{
    
    
    char inbuffer[NUM];
    while(true)
    {
    
    
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
    
    
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    
    
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}
~tcpserver(){
    
    }
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}
  • The parent process forks to create a child process. After creation, waitpid waits for the child process to be recycled. The child process forks to create a grandson process and exits directly after creation. This causes the grandson process to become an orphan process and then be adopted by the OS. Therefore, unless the client exits the IO task, the orphan process will continue to run without interfering with other processes, that is, the communication between the server and the client is completed in parallel.

  • Note that when the server accepts a client's connection request, it needs to apply for a file descriptor, and the file descriptor has an upper limit. If a large number of client requests are connected successfully and do not end, the file descriptor will be leaked.

image-20230825163937980

Therefore, unused file descriptors need to be closed in the parent process.

image-20230825192206974

  • The parent process recycles the child process here, and non-blocking waiting cannot be used. The reason is that the essence of non-blocking waiting is polling, and using it here will cause the parent process to block at the accept function and wait for the client to send a connection request, then the parent process cannot The child process is recycled. Therefore, the return value of waitpid is received with ret. When the recovery is successful, the log will be printed, and if it fails, it will be skipped.
  • When the child process graph exits or is terminated, the child process will send signal SIGCHILD No. 17 to the parent process. The parent process can ignore signal SIGCHILD No. 17 to avoid blocking and waiting for the child process to be recycled (this method is available for the Linux environment, and the rest is not ensure)
signal(SIGCHLD, SIG_IGN);

netstat to view network information

netstatIs a command line tool for viewing network connections and network statistics. It can be used to display network connections, routing tables, interface statistics, and more on the current system. In Linux systems, netstatthe command usage is as follows:

netstat [options]

Some commonly used options include:

  • -a: Display all connections, including listening and established connections.
  • -t: Display the connection of TCP protocol.
  • -u: Display the connection of UDP protocol.
  • -n: Display the IP address and port number numerically instead of attempting DNS resolution.
  • -p: Displays process information associated with the connection.
  • -r: Display the routing table.
  • -l: Only the listening connections are displayed.
  • -atun: Display all TCP and UDP connections

image-20230825190401543

Note: There are two connections here because the server and client are on the same host, that is, the server and client have completed local loopback, so two connections can be seen.

3. Multi-threaded version: parallel execution

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    
    
    enum
    {
    
    
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver;
    class ThreadData
    {
    
    
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){
    
    }
tcpserver* _this;
int _psock;
    };

class tcpserver
{
    
    
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){
    
    }

void initserver()
{
    
    
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    
    
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    
    
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    
    
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    
    
    while(true)
    {
    
    
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
    
    
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        //多线程版
pthread_t pid;
ThreadData* th=new ThreadData(this,sock);
pthread_create(&pid,nullptr,start_routine,th);
    }
}
 static void* start_routine(void* args)
{
    
    
    pthread_detach(pthread_self());//线程分离后让OS自动回收新线程
    ThreadData* ret=static_cast<ThreadData*>(args);
    ret->_this->serviceIO(ret->_psock);
    close(ret->_psock);
    delete ret;
    return nullptr;
}    
    
void serviceIO(int sock)
{
    
    
    char inbuffer[NUM];
    while(true)
    {
    
    
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
    
    
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    
    
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}
~tcpserver(){
    
    }
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}
  • When the server receives a connection request from a client, it applies for a new thread. Multi-threading allows the server to receive multiple threads.

log.hpp

#pragma once

#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>
 #include <unistd.h>
 #include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(int level)
{
    
    
    switch(level)
    {
    
    
        case DEBUG: return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default: return nullptr;
    }
}

void logMessage(int level, const char* format,...)
{
    
    
    // [日志等级] [时间戳/时间] [pid] [messge]
    // [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]

    // 暂定
  //  std::cout << message << std::endl;

char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());//把后面的内容打印进logprestr缓存区中

char logeldstr[NUM];
va_list arg;
va_start(arg,format); 
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函数列表中的...

  cout<<logprestr<<logeldstr<<endl;

//  FILE* str=fopen(LOG_STR,"a");
//  FILE* err=fopen(LOG_ERR,"a");//以追加方式打开文件,若文件不存在则创建
 
//  if(str!=nullptr||err!=nullptr)//两个文件指针都不为空则创建文件成功
//  {
    
    
//   FILE* ptr=nullptr;
//   if(level==DEBUG||level==NORMAL||level==WARNING)
//   {
    
    
//     ptr=str;
//   }
//    if(level==ERROR||level==FATAL)
//   {
    
    
//     ptr=err;
//   }
//   if(ptr!=nullptr)
//   {
    
    
//     fprintf(ptr,"%s-%s\n",logprestr,logeldstr);
//   }
//   fclose(str);
//   fclose(err);
 //}

}

Variable parameter list

  • va_list is a (char*) renamed type that defines variables that can access variadic parameters.

  • va_start(ap, v) ap is a defined variable parameter variable, v is the first parameter name before the variable parameter in the formal parameter, and its function is to make ap point to the variable parameter part.

  • va_arg(ap, t) ap is the defined variable parameter variable, t is the type of variable parameter, and according to the type, the data in the variable parameter list is accessed.

  • va_end(ap) ap is a defined variable parameter variable, which makes the ap variable blank and used as an end.

vsnprintf function prototype

#include <stdio.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • str is a pointer to a character array (buffer) used to store formatted data

  • size is the size of the buffer, limiting the maximum number of characters written, including the terminating null character

  • format format string, similar to printfthe format string in the function

  • ap is a va_listtype variable used to store information about the variable parameter list, and it should be noted that the order in which the OS pushes parameters onto the stack is from right to left.

  • vsnprintfformatThe function writes data to a buffer according to the specified format string str, but does not exceed the specified buffer size. It automatically adds a null termination character to the end of the buffer after writing data, ensuring that the result is a legal C string.

image-20230825224540368

daemon

Daemon is a special type of process that runs in the background in a computer system. It is typically initialized when the operating system starts and remains active throughout the system's runtime without user interaction. Daemon processes are usually used to perform system tasks, service management, and provide background services, such as network services, scheduled tasks, etc.

The characteristics of the daemon process are as follows:

  1. Running in the background , the daemon runs in the background without interacting with the user and without a controlling terminal.
  2. Independence : It is usually independent of the user session, and the daemon will continue to run even if the user logs out or closes the terminal.
  3. No standard input and output : Daemon processes usually have no standard input and output because they do not interact with the user. They usually write output to log files.
  4. Detach itself : The daemon goes through a series of operations to disconnect from the terminal, session, and control group to ensure that it is not accidentally closed by the controlling terminal.

A server can have multiple sessions. For example, there is a root user and multiple ordinary users on a server. When an ordinary user logs in to the server, it becomes a session.

A session can have multiple background tasks, but only one foreground task (bash).

image-20230825231612464

  • When viewing the tasks, you can see that task 1 is ./tcpserver, task 2 is sleep 1000 | sleep 2000 | sleep 3000 &, task 3 is sleep 4000 | sleep 5000 &, and all three tasks are followed by &, after the process or task . The function of & is to put the task to run in the background.
  • The parent processes of sleep 1000, sleep 2000, sleep 3000, sleep 4000, and sleep 5000 are all 16853, which is bash; and the PGID of sleep 1000, sleep 2000, and sleep 3000 are the same, and they are all the pid of sleep 1000, that is, sleep 1000, sleep 200 0, sleep 3000 belongs to the same group, and the same group needs to work together to complete the same job. The pid of the first task is the pid of the team leader, which is the pid of sleep 1000; and the SIDs of group 16858 and group 17070 are both 16853, that is, the two groups belong to the same session (bash) and want to complete the same task;
fg、bg
  1. fg job number: put the job in the foreground

  2. bg job number: put the job in the background, or continue to execute the background job

  3. ctrl+Z pauses the foreground task and puts the job in the background

image-20230825233513024

  • When a user logs in, the server needs to create some background jobs and foreground jobs (command line) to serve the user, and the user's logout or exit from the server will also affect its foreground and background jobs. The server program cannot be affected by user login and logout.
  • We can make the server program form its own session and process group , then the program will have nothing to do with the terminal device and will no longer be affected by user login and logout. This type of process is called a daemon process

site

In Unix and Unix-like systems, setsida system call function used to create a new session. A session is a group of related processes, usually consisting of a control terminal and some child processes. setsidThe main function of the function is to detach the process calling it from the current session and create a new session.

 #include <unistd.h>
pid_t setsid(void);
  • Create a new session : The calling setsidprocess will become the session leader of a new session. The new session is no longer associated with the previous controlling terminal. But the process cannot be the group leader before calling the setsid function.
  • Detached terminal : The calling setsidprocess is no longer associated with any controlling terminal and cannot regain the controlling terminal.
  • Become the leader of the new process group : The first process in the new session (the calling setsidprocess) becomes the leader of the new process group.

daemon.hpp

#pragma once

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
    
    
    // 1. 让调用进程忽略掉异常的信号
signal(SIGPIPE,SIG_IGN);//选择忽略SIGPIPE信号
    // 2. 如何让自己不是组长,setsid
if(fork()>0)
exit(0);//父进程退出
    // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
pid_t ret=setsid();
assert(ret!=-1);
    // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
int fd=open(DEV,O_RDWR);
if(fd>=0)
{
    
    
    //dup2(oldfd,newfd):将oldfd的内容填充到newfd中,这样输入到newfd的内容被重定向到oldfd
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
}else
{
    
    
    close(0);
    close(1);
    close(2);
}
    // 4. 可选:进程执行路径发生更改
if(currPath) chdir(currPath);//更改currPath的路径
}
  • /dev/nullIt is a special device file that is used as a data discard point. Data written to it will be discarded, and reading data from it will immediately return EOF (End of File).
  • SIGPIPE triggering scenario: When a process writes data to a pipe (or socket) that has closed the write end, or when the process sends data to a socket that has received a packet (connection reset), the process RSTwill Send the SIGPIPE signal to the parent process to terminate the process. Ignoring SIGPIPE prevents the process from writing data to /dev/null and terminating the process due to errors.
  • The parent process creates a child process, and the parent process serves as the group leader. After the parent process exits, the child process can become the group leader itself, that is, it can become a daemon process.
  • dup2(oldfd,newfd): Fill the content of oldfd into newfd, so that the content input to newfd is redirected to oldfd. In the code, the contents of input file descriptor 012 are redirected to fd, that is, /dev/null.

tcpserver.cc

#include"tcpserver.hpp"
#include"log.hpp"
#include"daemon.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;

static void Usage(string proc)
{
    
    
    cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}

int main(int argc,char* argv[])
{
    
    
if(argc!=2)
{
    
    
    Usage(argv[0]);
    exit(USAGE_ERR);
}

uint16_t port=atoi(argv[1]);//将字符串转化为整数

unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
daemonSelf();
ts->start();

return 0;
}

image-20230826112937206

TCP protocol communication process

image-20230902193547174

Server initialization is required first

image-20230902162505124

Server initialization:

  • Call the socket to create a file descriptor, which is used for listening;
  • Call bind to bind the current file descriptor and ip/port together; if the port is already occupied by other processes, bind will fail;
  • Call listen to declare the current file descriptor as a server file descriptor. The file descriptor is in the listening state, waiting for the client to initiate a connection;
  • Call accecpt and block, waiting for the client to connect;

The process of establishing a connection (often called the three-way handshake)

The process of establishing a connection (including three-way handshake)

  • The client calls the socket and creates a file descriptor;
  • The client calls connect to initiate a request to the server at the specified address and port; (a three-way handshake will be performed during the request process)
  • connect will send a SYN segment to the server and block waiting for the server to respond; (first handshake)
  • After the server receives the SYN segment from the client, it will respond with a SYN-ACK segment to indicate "agree to establish the connection"; (second handshake)
  • After receiving SYN-ACK, the client will return from connet() and send a response ACK segment to the server; (third handshake)
  • After receiving the ACK segment from the client, the server will return from accpet() and return (allocate) a new file descriptor connfd for communication with the client.
  • It can be seen that the three-way handshake starts with a request initiated by connect() and ends with the return of connect(). Therefore, when the client calls connect(), it essentially performs a three-way handshake with the server in some way.

image-20230902163100168

**For the three-way handshake to establish a link, **it is mainly to initialize the initial value of Sequence Number. Both parties in the communication must inform each other of their initialized Sequence Number (abbreviated as ISN: Initial Sequence Number) - so it is called SYN, the full name is Synchronize Sequence Numbers. That's x and y in the picture above. This number should be used as the sequence number for future data communication to ensure that the data received by the application layer will not be out of order due to transmission problems on the network (TCP will use this sequence number to splice data). Part of the interpretation of the three-way handshake from Mr. Chen Hao

  • After the connection is successfully established, it will be obtained by accpet, and the client and server can communicate. It should be noted that connection establishment is done by the three-way handshake. The three-way handshake is the work of the bottom layer of TCP. What acep needs to do is to take the established connection from the bottom layer to the user layer. That is, accept itself does not participate in the three-way handshake process. (Not involved in establishing a connection), accpet will block and wait to obtain the established connection. If the connection is not established, it will wait.

Data transfer process

image-20230902174502858

image-20230902174857760

  • After the connection is established, the TCP protocol provides full-duplex communication services; the so-called full-duplex means that in the same connection, at the same time, both communicating parties can write data at the same time; the reason is that the application layer and transport layer of the server and client There are two buffers, one is the sending buffer and the other is the receiving buffer, so the sending and reading of the server and the client will not affect each other. The relative concept is called half-duplex. On the same connection at the same time, only one party can write data;

  • The server calls read() immediately after returning from accept(). Reading the socket is like reading a pipe. If no data arrives, it blocks and waits;

  • At this time, the client calls write() to send a request to the server. After receiving it, the server returns from read() to process the client's request. During this period, the client calls read() to block and wait for the server's response;

  • The server calls write() to send the processing results back to the client, and calls read() again to block and wait for the next request;

  • After receiving it, the client returns from read() and sends the next request, and the cycle continues.

Disconnection process

image-20230902192244687

  • If the client has no more requests, it calls close() to close the connection, and the client will send the FIN segment to the server; (first handshake)
  • At this time, after the server receives the FIN, it will respond with an ACK, and read will return 0; (second handshake)
  • After read returns, the server knows that the client has closed the connection, and also calls close to close the connection. At this time, the server will send a FIN to the client; (third handshake)
  • The client receives the FIN and returns an ACK to the server; (the fourth handshake)
  • This disconnection process is often called the four-wave wave

  • The 4 wave waves are actually 2 if you look carefully, because TCP is full-duplex, so both the sender and the receiver need Fin and Ack. However, one party is passive, so it looks like the so-called 4 wave waves. If both sides disconnect at the same time, it will enter the CLOSING state and then reach the TIME_WAIT state.

Reasons why the client needs to disconnect when it is not communicating with the server

  • In fact, transmission on the network is not connected, including TCP . The so-called "connection" of TCP is actually just maintaining a "connection state" on both sides of the communication, making it look like there is a connection. Therefore, TCP state transition is very important. If the connection is not disconnected in time after the communication is completed, the resources of the operating system will be occupied and not used, resulting in less and less system resources.
  • The server can establish connections with multiple clients, which means that the server will receive a large number of connections. Therefore, the operating system must manage these connections, that is, "organize first and then manage". The server needs to maintain connection-related data structures. By organizing these data structures, the management of connections is transformed into the management of data structures.
  • The operating system needs to maintain the data structures related to these connections, which will inevitably consume resources. If the non-communicating connections are not disconnected, it will lead to a waste of resources in the operating system. One of the differences between TCP and UDP is that TCP needs to manage connection-related resources.

Guess you like

Origin blog.csdn.net/m0_71841506/article/details/132509902