ネットワーク バイト オーダー - TCP インターフェイスとその単純な TCP サーバーとしての実装

ネットワーク バイト オーダー - TCP インターフェイスとその単純な TCP サーバーとしての実装

森格 (2)

単純な TCP サーバーの実装

  • TCP と UDP の違いは、ソケットを監視状態に設定する必要があることです。つまり、TCP はリンク指向であるため、TCP ソケットを待機状態に設定する必要があります。
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");
}

ソケット関数のプロトタイプ

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

int socket(int domain, int type, int protocol);
  • domainプロトコル ファミリを表します。一般的に使用されるのはAF_INET(IPv4) とAF_INET6(IPv6) です。

  • typeソケットのタイプを示します。一般的に使用されるのはSOCK_STREAM(TCP) とSOCK_DGRAM(UDP) です。

  • protocol通常は 0 に設定して、システムがdomainとに基づいてtype適切なプロトコルを選択できるようにします。

  • socket() はネットワーク通信ポートを開き、成功すると open() と同様にファイル記述子を返します。

  • アプリケーションは、ファイルの読み取りと書き込みと同様に、読み取り/書き込みを使用して、ソケット関数を通じてネットワーク上でデータを送受信できます。

バインド関数のプロトタイプ

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfdソケット記述子です。

  • addrstruct sockaddrバインドする IP アドレスとポート情報を含む構造体です。

  • addrlenaddr構造の長さです。addr 構造体は複数のプロトコルの sockaddr 構造体を受け入れることができるため、その構造体の長さを渡す必要があります。

  • bind() は成功すると 0 を返し、失敗すると -1 を返します。

  • bind() の機能は、ネットワーク通信に使用されるファイル記述子である sockfd が addr によって記述されたアドレスとポート番号をリッスンできるように、パラメータ sockfd と myaddr をバインドすることです。

listen 関数のプロトタイプ

int listen(int sockfd, int backlog);
  • sockfdソケット記述子であり、ネットワーク監視に使用されるファイル記述子を指します。
  • backlog接続を待機するキューの最大長を示します。
  • listen は成功すると 0 を返し、失敗すると -1 を返します。
  • listen 関数は、sockfd を listen 状態にし、backlog クライアントを接続待機状態にします。backlog クライアントの接続要求を超えた場合、接続要求は無視されます。
  • 実際、listen 関数は、指定されたソケット sockfd が listen 状態にあることをオペレーティング システムに伝えます。ソケットは、他のコンピュータがネットワーク経由でソケットとの接続を確立するのを待ち始めます。接続要求が到着すると、オペレーティング システムは接続リクエストを接続キューに入れて接続します キューの最大長はバックログです 接続キューは接続リクエストを格納するバッファです キューがいっぱいの場合、新しい接続リクエストは拒否されます つまり、ソケットが待機状態にある場合、ソケットはデータ送信を直接処理せず、他のコンピュータが接続を開始するのを待ちます。

一般に、initserver 関数は、最初にソケットを作成し、次に指定されたポート番号と IP を入力し、ソケットをリスニング状態に設定します。

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;
        }

関数プロトタイプを受け入れる

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

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: これは、socket関数を通じて作成され、すでにリスニング状態になっているソケット記述子であり、受信接続要求を監視するために使用されます。

  • addr: はstruct sockaddr、接続要求のクライアントのアドレス情報を受け取るために使用される構造体へのポインタです。

  • addrlen: はsocklen_t型へのポインタで、addrバッファの長さを指定するために使用され、実際のクライアント アドレス構造のサイズを返すためにも使用されます。

  • accept 関数は、受信した接続要求を受け入れる機能であり、接続要求が到着するまでプログラムの実行をブロックします。接続要求が到着すると、新しいソケットが作成され、この新しいソケットのファイル記述子が返されます。この新しいソケットはクライアントとの通信に使用され、クライアントのアドレス情報が設定されaddrますaddrlen

  • サーバー プログラムでは、accept 関数をループ内で使用して、複数のクライアントからの接続要求を受け入れます。

start 関数の機能は、クライアントから送信された接続要求の受け入れをブロックし、サーバーがクライアントとの通信を確立できるようにすることです。

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. シングルプロセス版:クライアントシリアル版

#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;//端口号
};

}

注: データをサーバーにシリアル送信するときに、クライアントはどこでブロックされますか? accept 関数でブロックされているため、accept はクライアントがアクセスするのを待ちます。これはブロッキング待ちです。accept 関数が接続要求を受信した後、後続のクライアント接続要求は accept 関数で待機する必要があります。前のクライアントが終了すると、サーバーは現在のクライアントによって送信された接続要求を正常に受け入れ、現在のクライアントのデータを受信できます。つまり、サーバーはクライアントから送信されたデータをシリアルに受信して処理します。

2. マルチプロセス版:クライアント並列版

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;//端口号
};

}
  • 親プロセスはフォークして子プロセスを作成し、作成後、waitpid は子プロセスがリサイクルされるのを待ちます。子プロセスはフォークして孫プロセスを作成し、作成後すぐに終了します。これにより、孫プロセスが孤立プロセスになり、OS に採用されます。したがって、クライアントが IO タスクを終了しない限り、孤立プロセスは他のプロセスを妨げることなく実行され続けます。つまり、サーバーとクライアント間の通信は並行して完了します。

  • サーバーがクライアントの接続要求を受け入れる場合、ファイル記述子を申請する必要があり、ファイル記述子には上限があり、多数のクライアント要求が接続に成功しても終了しない場合、ファイル記述子が漏洩することに注意してください。 。

画像-20230825163937980

したがって、未使用のファイル記述子は親プロセスで閉じる必要があります。

画像-20230825192206974

  • ここでは親プロセスが子プロセスを再利用するため、ノンブロッキング待機は使えません 理由は、ノンブロッキング待機の本質はポーリングであり、ここで使用すると親プロセスがaccept関数でブロックして待機してしまうためです。クライアントが接続要求を送信すると、親プロセスは送信できなくなり、子プロセスがリサイクルされます。そのため、waitpid の戻り値を ret で受け取り、リカバリが成功した場合はログが出力され、失敗した場合はスキップされます。
  • 子プロセス グラフが終了または終了すると、子プロセスはシグナル SIGCHILD No. 17 を親プロセスに送信します。親プロセスは、ブロックされて子プロセスがリサイクルされるのを待つことを避けるために、シグナル SIGCHILD No. 17 を無視できます (このメソッド) Linux環境では利用可能ですが、残りは未確認です)
signal(SIGCHLD, SIG_IGN);

netstat : ネットワーク情報を表示する

netstatネットワーク接続とネットワーク統計を表示するためのコマンド ライン ツールです。現在のシステム上のネットワーク接続、ルーティング テーブル、インターフェイス統計などを表示するために使用できます。Linux システムでは、netstatコマンドの使用法は次のとおりです。

netstat [options]

よく使用されるオプションには次のようなものがあります。

  • -a: リスニング接続と確立された接続を含むすべての接続を表示します。
  • -t:TCPプロトコルの接続を表示します。
  • -u:UDPプロトコルの接続を表示します。
  • -n: DNS 解決を試行する代わりに、IP アドレスとポート番号を数値で表示します。
  • -p:接続に関連付けられたプロセス情報を表示します。
  • -r:ルーティングテーブルを表示します。
  • -l: リスニング接続のみが表示されます。
  • -atun: すべての TCP および UDP 接続を表示します

画像-20230825190401543

注: サーバーとクライアントが同じホスト上にあるため、ここでは 2 つの接続が存在します。つまり、サーバーとクライアントがローカル ループバックを完了しているため、2 つの接続が表示されます。

3. マルチスレッドバージョン: 並列実行

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;//端口号
};

}
  • サーバーはクライアントから接続要求を受信すると、新しいスレッドを申請します。マルチスレッドにより、サーバーは複数のスレッドを受信できます。

ログ.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);
 //}

}

可変パラメータ一覧

  • va_list は、可変引数パラメーターにアクセスできる変数を定義する (char*) 名前変更された型です。

  • va_start(ap, v) ap は定義済みの変数パラメータ変数、v は仮パラメータの変数パラメータの前の最初のパラメータ名で、その機能は ap に変数パラメータ部分を指すようにすることです。

  • va_arg(ap, t) ap は定義された変数パラメータの変数、t は変数パラメータの型で、型に応じて変数パラメータリスト内のデータにアクセスします。

  • va_end(ap) ap は定義済みの変数パラメータ変数であり、ap 変数を空白にし、end として使用します。

vsnprintf関数プロトタイプ

#include <stdio.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • str は、フォーマットされたデータを格納するために使用される文字配列 (バッファ) へのポインタです。

  • size はバッファのサイズで、終端の null 文字を含む、書き込まれる最大文字数を制限します。

  • format フォーマット文字列。printf関数のフォーマット文字列に似ています。

  • ap はva_list変数パラメータ リストに関する情報を格納するために使用される型変数であり、OS がパラメータをスタックにプッシュする順序は右から左であることに注意してください。

  • vsnprintfこの関数は、指定された形式文字列に従ってformatデータをバッファに書き込みますstrが、指定されたバッファ サイズを超えません。データの書き込み後にバッファの末尾に NULL 終了文字が自動的に追加され、結果が正当な C 文字列であることが保証されます。

画像-20230825224540368

デーモン

デーモンは、コンピュータ システムのバックグラウンドで実行される特別なタイプのプロセスです。通常、オペレーティング システムの起動時に初期化され、ユーザーの操作なしでシステムのランタイム全体を通じてアクティブなままになります。デーモン プロセスは通常、システム タスクやサービス管理を実行し、ネットワーク サービスやスケジュールされたタスクなどのバックグラウンド サービスを提供するために使用されます。

デーモンプロセスの特徴は以下のとおりです。

  1. バックグラウンドで実行されるデーモンは、ユーザーと対話したり、端末を制御したりすることなく、バックグラウンドで実行されます。
  2. 独立性: 通常、ユーザー セッションから独立しており、ユーザーがログアウトするかターミナルを閉じても、デーモンは実行を継続します。
  3. 標準入出力がない: デーモン プロセスはユーザーと対話しないため、通常、標準入出力がありません。通常、出力はログ ファイルに書き込まれます。
  4. 自身を切断する: デーモンは、制御端末によって誤って閉じられないように、一連の操作を実行して端末、セッション、および制御グループから切断します。

サーバーには複数のセッションを設定できます。たとえば、サーバー上に root ユーザーと複数の一般ユーザーが存在し、一般ユーザーがサーバーにログインするとセッションになります。

セッションには複数のバックグラウンド タスクを含めることができますが、フォアグラウンド タスク(bash) は 1 つだけです。

画像-20230825231612464

  • タスクを表示すると、タスク 1 は ./tcpserver、タスク 2 は sleep 1000 | sleep 2000 | sleep 3000 &、タスク 3 は sleep 4000 | sleep 5000 & であり、3 つのタスクすべての後に & が続いていることがわかります。 process または task 。 & の機能は、タスクをバックグラウンドで実行することです。
  • スリープ 1000、スリープ 2000、スリープ 3000、スリープ 4000、およびスリープ 5000 の親プロセスはすべて 16853 (bash) であり、スリープ 1000、スリープ 2000、およびスリープ 3000 の PGID は同じであり、それらはすべて pid です。スリープ 1000 のうち、スリープ 1000、スリープ 200 0、スリープ 3000 は同じグループに属しており、同じジョブを完了するには同じグループが協力する必要があります。最初のタスクの pid はチーム リーダーの pid、つまりスリープ 1000 の pid であり、グループ 16858 とグループ 17070 の SID は両方とも 16853 です。つまり、2 つのグループは同じセッション (bash) に属しており、同じタスクを完了したい。
背景、背景
  1. fg ジョブ番号: ジョブをフォアグラウンドに置きます

  2. bg ジョブ番号: ジョブをバックグラウンドに置くか、バックグラウンド ジョブの実行を継続します。

  3. Ctrl+Z はフォアグラウンド タスクを一時停止し、ジョブをバックグラウンドに置きます

画像-20230825233513024

  • ユーザーがログインすると、サーバーはユーザーにサービスを提供するためにいくつかのバックグラウンド ジョブとフォアグラウンド ジョブ (コマンド ライン) を作成する必要があります。ユーザーのログアウトまたはサーバーからの終了も、フォアグラウンド ジョブとバックグラウンド ジョブに影響します。サーバー プログラムはユーザーのログインとログアウトの影響を受けません。
  • サーバープログラムに独自のセッションとプロセスグループを形成させることができます。そうすれば、プログラムは端末デバイスとは何の関係もなくなり、ユーザーのログインとログアウトの影響を受けなくなります。このタイプのプロセスはデーモン プロセスと呼ばれます

サイト

Unix および Unix 系システムにおいて、setsid新しいセッションを作成するために使用されるシステム コール関数。セッションは関連するプロセスのグループであり、通常は制御端末といくつかの子プロセスで構成されます。setsidこの関数の主な機能は、それを呼び出しているプロセスを現在のセッションから切り離し、新しいセッションを作成することです。

 #include <unistd.h>
pid_t setsid(void);
  • 新しいセッションの作成: 呼び出し元のsetsidプロセスが新しいセッションのセッション リーダーになります。新しいセッションは、以前の制御端末と関連付けられなくなりました。ただし、setsid 関数を呼び出す前にプロセスをグループ リーダーにすることはできません。
  • 切り離された端末: 呼び出し側setsidプロセスは制御端末と関連付けられなくなり、制御端末を取り戻すことができなくなります。
  • 新しいプロセス グループのリーダーになる: 新しいセッションの最初のプロセス (呼び出しsetsidプロセス) が、新しいプロセス グループのリーダーになります。

デーモン.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/nullデータの破棄ポイントとして使用される特殊なデバイスファイルで、書き込まれたデータは破棄され、データを読み出すとすぐにEOF(End of File)が返されます。
  • SIGPIPE トリガー シナリオ: プロセスが書き込みエンドを閉じたパイプ (またはソケット) にデータを書き込むとき、またはプロセスがパケットを受信したソケットにデータを送信するとき (接続リセット)、プロセスは SIGPIPE シグナルを次の宛先に送信しますRST。親プロセスを呼び出してプロセスを終了します。SIGPIPE を無視すると、プロセスが /dev/null にデータを書き込んだり、エラーが発生してプロセスを終了したりすることがなくなります。
  • 親プロセスは子プロセスを作成し、その親プロセスがグループ リーダーとして機能します。親プロセスが終了すると、子プロセス自体がグループ リーダー、つまりデーモン プロセスになることができます。
  • dup2(oldfd,newfd): newfd に入力されたコンテンツが oldfd にリダイレクトされるように、oldfd のコンテンツを newfd に埋め込みます。コードでは、入力ファイル記述子 012 の内容が fd、つまり /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;
}

画像-20230826112937206

TCPプロトコルの通信処理

画像-20230902193547174

最初にサーバーの初期化が必要です

画像-20230902162505124

サーバーの初期化:

  • ソケットを呼び出して、リッスンに使用されるファイル記述子を作成します。
  • 現在のファイル記述子と IP/ポートを一緒にバインドするには、bind を呼び出します。ポートがすでに他のプロセスによって占有されている場合、バインドは失敗します。
  • listen を呼び出して、現在のファイル記述子をサーバー ファイル記述子として宣言します。ファイル記述子は待機状態になり、クライアントが接続を開始するのを待ちます。
  • accecpt を呼び出してブロックし、クライアントが接続するのを待ちます。

接続を確立するプロセス (スリーウェイ ハンドシェイクと呼ばれることが多い)

接続を確立するプロセス (スリーウェイ ハンドシェイクを含む)

  • クライアントはソケットを呼び出してファイル記述子を作成します。
  • クライアントは connect を呼び出し、指定されたアドレスとポートでサーバーへのリクエストを開始します (リクエスト プロセス中に 3 ウェイ ハンドシェイクが実行されます)。
  • connect は SYN セグメントをサーバーに送信し、サーバーが応答するまでブロックします (最初のハンドシェイク)。
  • サーバーはクライアントから SYN セグメントを受信すると、「接続の確立に同意した」ことを示す SYN-ACK セグメントで応答します (2 回目のハンドシェイク)。
  • SYN-ACK を受信した後、クライアントは connet() から戻り、応答 ACK セグメントをサーバーに送信します (3 回目のハンドシェイク)。
  • クライアントから ACK セグメントを受信した後、サーバーは accpet() から戻り、クライアントとの通信用に新しいファイル記述子 connfd を返します (割り当てます)。
  • スリーウェイ ハンドシェイクは connect() によって開始されたリクエストで始まり、connect() の戻りで終了することがわかります。したがって、クライアントが connect() を呼び出すと、基本的にサーバーとの 3 ウェイ ハンドシェイクが実行されます。何らかの方法で。

画像-20230902163100168

**リンクを確立するための 3 ウェイ ハンドシェイクでは、**主に Sequence Number の初期値を初期化します。通信の両当事者は、初期化されたシーケンス番号 (ISN: Initial Sequence Number と略記) を互いに通知する必要があります。そのため、これは SYN と呼ばれ、正式名は Synchronize Sequence Numbers です。上の図の x と y がそれです。この番号は、ネットワーク上の送信の問題によってアプリケーション層で受信したデータの順序が狂わないようにするために、今後のデータ通信のシーケンス番号として使用する必要があります (TCP はこのシーケンス番号を使用してデータを結合します)。チェン・ハオ氏からの三者握手の解釈の一部

  • 接続が正常に確立されると、accpet によって接続が取得され、クライアントとサーバーは通信できるようになります。接続の確立は 3 ウェイ ハンドシェイクによって行われることに注意してください。3 ウェイ ハンドシェイクは TCP の最下層の作業です。accept が行う必要があるのは、確立された接続を最下層からユーザー層に取り込むことです。つまり、accept 自体は 3 ウェイ ハンドシェイク プロセスに参加しません (接続の確立には関与しません)。accpet はブロックされ、確立された接続を取得するまで待機します。接続が確立されていない場合は待機します。

データ転送プロセス

画像-20230902174502858

画像-20230902174857760

  • 接続が確立された後、TCP プロトコルは全二重通信サービスを提供します。いわゆる全二重とは、同じ接続で、同時に通信する両方の当事者が同時にデータを書き込むことができることを意味します。その理由は次のとおりです。サーバーとクライアントのアプリケーション層とトランスポート層 バッファーは 2 つあり、1 つは送信バッファー、もう 1 つは受信バッファーであるため、サーバーとクライアントの送信と読み取りは相互に影響しません。相対的な概念は半二重と呼ばれ、同じ接続上では同時に一方の当事者だけがデータを書き込むことができます。

  • サーバーは accept() から戻った直後に read() を呼び出します。ソケットの読み取りはパイプの読み取りと似ています。データが到着しない場合はブロックして待機します。

  • このとき、クライアントは write() を呼び出してサーバーにリクエストを送信し、それを受信したサーバーは read() から戻り、クライアントのリクエストを処理します。この間、クライアントは read() を呼び出してブロックして待機します。サーバーの応答。

  • サーバーは write() を呼び出して処理結果をクライアントに送り返し、再度 read() を呼び出してブロックして次のリクエストを待ちます。

  • それを受信した後、クライアントは read() から戻り、次のリクエストを送信し、サイクルが継続します。

切断処理

画像-20230902192244687

  • クライアントにそれ以上のリクエストがない場合、close() を呼び出して接続を閉じ、クライアントは FIN セグメントをサーバーに送信します (最初のハンドシェイク)。
  • このとき、サーバーは FIN を受信した後、ACK で応答し、read は 0 を返します (2 回目のハンドシェイク)。
  • read が戻った後、サーバーはクライアントが接続を閉じたことを認識し、close を呼び出して接続を閉じます。このとき、サーバーは FIN をクライアントに送信します (3 回目のハンドシェイク)。
  • クライアントは FIN を受信し、サーバーに ACK を返します (4 回目のハンドシェイク)。
  • この切断プロセスは、しばしば4 波と呼ばれます。

  • TCP は全二重なので、送信側と受信側の両方に Fin と Ack が必要です。注意して見ると、4 つの波は実際には 2 つになります。ただし、一方が消極的なので、いわゆる4波動のような感じになります。両方が同時に切断されると、CLOSING 状態になり、その後 TIME_WAIT 状態に達します。

クライアントがサーバーと通信していないときに切断する必要がある理由

  • 実際には、TCP を含め、ネットワーク上の送信は接続されていませんTCP のいわゆる「接続」は、実際には通信の両側で「接続状態」を維持し、接続があるように見せているだけです。したがって、TCP の状態遷移は非常に重要です。通信完了後、時間内に接続を切断しないと、オペレーティング システムのリソースが占有されて使用されなくなり、システム リソースが減少していきます。
  • サーバーは複数のクライアントとの接続を確立できます。これは、サーバーが多数の接続を受信することを意味します。したがって、オペレーティング システムはこれらの接続を管理する必要があります。つまり、「最初に整理してから管理する」必要があります。サーバーは接続を維持する必要があります。これらのデータ構造を整理することにより、接続の管理がデータ構造の管理に変換されます。
  • オペレーティング システムはこれらの接続に関連するデータ構造を維持する必要があるため、必然的にリソースが消費され、通信していない接続が切断されないと、オペレーティング システムのリソースが無駄に消費されます。TCP と UDP の違いの 1 つは、TCP が接続関連のリソースを管理する必要があることです。

おすすめ

転載: blog.csdn.net/m0_71841506/article/details/132509902