Linuxでのソケットサーバーとクライアントの簡単な実装

目次

1.Linuxファイル記述子

2.Linuxでソケットを作成します

3. bind()関数とconnect()関数

3.1、bind()関数

3.2、connect()関数

4. listen()関数とaccept()関数

4.1、listen()関数

4.2、accept()関数

5、write()和read()

5.1、write()関数

5.2、read()関数

6、send()およびrecev()

7、サービスとクライアントの簡単な実装


1.Linuxファイル記述子

Linuxでは、すべてがファイルです。ハードウェアデバイスは、デバイスファイルと呼ばれる仮想ファイルにマップすることもできます。たとえば、stdinは標準入力ファイルと呼ばれ、対応するハードウェアデバイスは通常キーボードであり、stdoutは標準出力ファイルと呼ばれ、対応するハードウェアデバイスは通常ディスプレイです。すべてのファイルについて、read()関数を使用してデータを読み取り、write()関数を使用してデータを書き込むことができます。

「すべてがファイルである」という考え方は、プログラマーの理解と操作を大幅に簡素化し、ハードウェアデバイスの処理を通常のファイルと同じようにします。Linuxで作成されたすべてのファイルには、ファイル記述子と呼ばれるint型番号があります。ファイルを使用する場合、必要なのはファイル記述子だけです。たとえば、stdinの記述子は0で、stdoutの記述子は1です。

Linuxでは、ソケットもファイルの一種と見なされており、通常のファイル操作と変わらないため、ファイルI / Oに関連する機能は当然ネットワークデータ転送の過程で使用できます。2台のコンピュータ間の通信は、実際には2つのソケットファイルの相互の読み取りと書き込みであると見なすことができます。

ファイル記述子はファイルハンドルと呼ばれることもありますが、「ハンドル」は主にWindowsの用語です。

2.Linuxでソケットを作成します

Linuxでは、<sys / socket.h>ヘッダーファイルのsocket()関数を使用してソケットを作成します。プロトタイプは次のとおりです。

int socket(int af, int type, int protocol);
  • af:アドレスファミリ、つまりIPアドレスタイプ。一般的に使用されるAF_INETおよびAF_INET6。AFは「AddressFamily」の略で、INETは「Internet」の略です。AF_INETはIPv4アドレスを表します。たとえば、127.0.0.1; AF_INET6は、1030 :: C9B4:FF12:48AA:1A2BなどのIPv6アドレスを表します。PFプレフィックスを使用することもできます。PFは「プロトコルファミリ」の略で、AFと同じです。たとえば、PF_INETはAF_INETと同等であり、PF_INET6はAF_INRT6と同等です。
  • タイプ:データ転送方法。一般的に使用されるのはSOCK_STREAMとSOCK_DGRAMです。
  • プロトコルは伝送プロトコルを表します。一般的に使用されるものはIPPROTO_TCPとIPPTOTO_UDPで、それぞれTCP伝送プロトコルとUDPプロトコルを表します。

これを見て、疑問があるかもしれませんが、IPアドレスの種類とデータ転送方法では、使用するプロトコルを決めるだけでは不十分ですか?3番目のパラメーターが必要なのはなぜですか?

そうです、通常の状況では、afとtypeの2つのパラメータを使用してソケットを作成できます。そのような状況が発生しない限り、オペレーティングシステムはプロトコルタイプを自動的に推測します。同じIPアドレスタイプをサポートする2つの異なるプロトコルがあります。データ送信方法。使用するプロトコルがわからない場合、オペレーティングシステムはそれを自動的に差し引くことはできません。

afの値がPF_INETに設定され、SOCK_STREAM送信方式が使用されている場合、これら2つの条件を満たすプロトコルはTCPのみであるため、SOCK()関数は次のように呼び出すことができます。

int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP);  //TCP套接字

afの値がPF_INETに設定され、SOCK_DGRAM送信方式が使用されている場合、これら2つの条件を満たすプロトコルはUDPのみであるため、SOCKET()関数は次のように呼び出すことができます。

int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字

上記の2つのケースでは、1つのプロトコルのみが条件を満たしています。protocolの値を0に設定すると、システムは使用するプロトコルを自動的に推測します。コードは次のとおりです。

int tcp_socket = socket(AD_INET,SOCK_STREAM,0);  //TCP套接字

int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字

3. bind()関数とconnect()関数

socket()関数は、ソケットを作成し、ソケットのさまざまな属性を決定するために使用されます。次に、サーバー側は、bind()関数を使用して、ソケットを特定のIPアドレスとポートにバインドする必要があります。 IPアドレスとポートのデータをソケットに渡すことができます。クライアントはconnect()関数を使用して接続を確立する必要があります。

3.1、bind()関数

bind()関数のプロトタイプは次のとおりです。

int bind(int sock,struct sockaddr *addr,socklen_t addrlen);

sockはソケットファイル記述子、addrはsockaddr構造体変数ポインター、addrlenはaddr変数のサイズです。

socklen_tの定義は 実際にはuint32です。

作成したソケットをIPアドレス128.0.0.1とポート1123にバインドするコードを見てみましょう。

int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO);  //创建一个TCP套接字

struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123);  //端口

bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));

sockaddr_in構造を見てみましょう

struct sockaddr_in
{
    sa_family_t    sin_family;  //地址族(Address Family),也就是地址类型
    unit16_t       sin_port;    //16位的端口号
    struct in_addr sin_addr;   //32位IP地址
    char           sin_zero[8]; //不使用,一般用0填充
}
  1. sin_familyとsocket()の最初のパラメーターの意味は同じであり、値も同じである必要があります。
  2. sin_portはポート番号、uint16_tの長さは2バイトです。理論的には、ポート番号の値の範囲は0〜65536ですが、ポート0〜1023は通常、システムによって特定のサービスプログラムに割り当てられます。 Webサービスのポートは70、FTPサービスのポートは21であるため、プログラムは1024〜65536のポート番号を割り当てようとします。
  3. sin_addrは、structin_addr構造体タイプの変数です。
  4. sin_zeroは8バイトを超えていますが、これは役に立たず、通常、memset()関数を使用して0で埋められます。上記のコードでは、最初にmemset()を使用して構造体のすべてのバイトを0で埋めてから、最初の3つのメンバーに値を割り当てます。残りのsin_zeroは当然0です。

in_addr構造

sockaddr_inの3番目のメンバーは、タイプin_addrの構造体であり、以下に示すように、1つのメンバーのみが含まれています。

struct in_addr
{
    in_addr_t s_addr;  //32位的IP地址
};

in_addr_tは、ヘッダーファイル<netinet / in.h>で定義されています。これは、unsigned longと同等で、長さは4バイトです。つまり、s_addrは整数であり、IPアドレスは文字列であるため、変換にはinet_addr()関数が必要です。次に例を示します。

unsigned long ip = inet_addr("127.0.0.1");

演算結果:

なぜsockaddrの代わりにsockaddr_inを使用するのですか?

bind()の2番目のパラメーターのタイプはsockaddrですが、コードではsockaddr_inが使用されているため、sockaddrに強制されます。

sockaddr構造体の定義は次のとおりです。

struct sockaddr
{
    sa_family_t sin_family;   //地址族
    char        sa_data[14];  //IP地址和端口号
}

次の図は、sockaddrとsockaddr_inの比較です(括弧内の数字は占有されているバイト数を示します)

sockaddrとsockaddr_inの長さは同じで、どちらも16バイトですが、sockaddrのsa_data領域では、「127.0.0.1:8080」のように、IPアドレスとポート番号を同時に指定する必要があります。この文字列を必要なものに変換するための関連関数はありません。sockaddrタイプの変数に直接値を割り当てることは難しいため、代わりにsockaddr_inを使用してください。これらの2つの構造体の長さは同じであり、型がキャストされたときにバイトが失われることはなく、バイトがなくなることもありません。

sockaddrは、複数のタイプのIPアドレスとポート番号を保護するために使用できる一般的な構造であり、sockaddr_inはIPv4を格納するために特に使用される構造であると考えることができます。さらに、IPv6アドレスを保存するために使用されるsockaddr_in6があります。その定義は次のとおりです。

struct sockaddr_in6
{
    sa_family_t sin6_family;   //IP地址类型,取值为AF_INET6
    in_port_t   sin6_port;     //16位端口号
    uint32_t sin6_flowinfo;    //IPv6流信息
    struct in6_addr sin6_addr; //具体的IPv6地址
    unit32_t sin6_scpoe_id;    //接口范围ID
};

in.hで宣言されているsockaddr_inとsockaddr_in6は次のとおりです。 

3.2、connect()関数

connect()関数は接続を確立するために使用され、そのプロトタイプは次のとおりです。

int connect(int sock,struct sockaddr *serv_addr,struct sockaddr*serv_addr,socklen_t addrlen);

各パラメーターの説明は、bind()関数と同じです。

4. listen()関数とaccept()関数

サーバー側プログラムの場合、bind()を使用してソケットをバインドした後、listen()関数を使用してソケットをパッシブリスニング状態にし、accept()関数を呼び出してクライアントに応答する必要があります。いつでもリクエストできます。

4.1、listen()関数

listen()関数を使用すると、ソケットをパッシブリスニング状態にすることができます。そのプロトタイプは次のとおりです。

int listen(int sock,int backlog)

sockはリスニング状態に入る必要があるソケットであり、backlogは要求キューの最大長です。

いわゆるパッシブモニタリングとは、クライアント要求がない場合、ソケットが「スリープ」状態になることを意味します。クライアント要求が受信された場合にのみ、ソケットは要求に応答するために「ウェイクアップ」されます。

リクエストキュー

ソケットがクライアント要求を処理しているときに、新しい要求が着信した場合、ソケットは処理できません。最初にバッファーに配置し、次に現在の要求が処理された後にバッファーから配置することしかできません。処理のために読み取ります。新しいリクエストが入ってくると、バッファがいっぱいになるまで順番にバッファのキューに入れられます。このバッファはリクエストキューと呼ばれます。

バッファーの長さ(格納できるクライアント要求の数)は、listen()関数のbacklogパラメーターで指定できますが、その量の標準はなく、ニーズによって異なります。

バックログの値がSOMAXCONNに設定されている場合、システムは要求キューの長さを決定します。この値は一般に比較的大きく、数百以上になる場合があります。

リクエストキューがいっぱいになると、新しいリクエストは受信されません。Linuxの場合、クライアントはECONNREFUSEDエラーを受け取ります。

注:listen()関数は、ソケットをリスニング状態に保つだけで、要求を受信しません。リクエストを受信するには、accept()関数を使用して、新しいリクエストが来るまでプロセスの実行をブロックする必要があります。

4.2、accept()関数

ソケットがリスニング状態の場合、クライアント要求はacceot()関数を介して受信できます。そのプロトタイプは次のとおりです。

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

そのパラメーターはlisten()およびconnect()と同じです。sockはサーバー側ソケット、addrはsockaddr_in構造変数、addrlenはパラメーターaddrの長さであり、sizeof()で取得できます。

accept()はクライアントと通信するための新しいソケットを返し、addrはクライアントのIPアドレスとポート番号を保存し、sockはサーバー側のソケットです。後でクライアントと通信するときは、元のサーバー側ソケットの代わりに、この新しく生成されたソケットを使用してください。

5、write()和read()

Linuxは、ソケットファイルと通常のファイルを区別しません。write()を使用してデータをソケットに書き込み、read()を使用してソケットからデータを読み取ります。

2台のコンピューター間の通信は、2つのソケット間の通信と同等です。サーバーでwrite()を使用してデータをソケットに書き込むと、クライアントはデータを受信し、read()を使用してソケットから接続します。単語を読み取った後、通信が完了しました。

5.1、write()関数

write()のプロトタイプは次のとおりです。

/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int  fd,const void *buf,size_t nbytes);

write()関数は、バッファbuf内のnbytesバイトをファイルfdに書き込みます。成功した場合は、書き込まれたバイト数を返し、失敗した場合は-1を返します。

5.2、read()関数

/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int  fd,void *buf,size_t nbytes);

read()関数は、fdファイルからnbytesバイトを読み取り、それらをバッファーbufに保存します。成功した場合は読み取ったバイト数を返し(ファイルの終わりに到達すると0を返します)、失敗した場合は-1を返します。 。

6、send()およびrecev()

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
                       int __flags, __CONST_SOCKADDR_ARG __addr,
                       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
                         int __flags, __SOCKADDR_ARG __addr,
                         socklen_t *__restrict __addr_len);


/* Send a message described MESSAGE on socket FD.
   Returns the number of bytes sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendmsg (int __fd, const struct msghdr *__message,
                        int __flags);

#ifdef __USE_GNU
/* Send a VLEN messages as described by VMESSAGES to socket FD.
   Returns the number of datagrams successfully written or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags);
#endif

/* Receive a message as described by MESSAGE from socket FD.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);

#ifdef __USE_GNU
/* Receive up to VLEN messages as described by VMESSAGES from socket FD.
   Returns the number of messages received or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags,
                     struct timespec *__tmo);
#endif

7、サービスとクライアントの簡単な実装

/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:service.c
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    while(1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

        //向客户端发送数据
        char str[] = "不要艾特我";
        write(clnt_sock, str, sizeof(str));

        //关闭套接字
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:client.cpp
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);

    printf("Message form server: %s\n", buffer);

    //关闭套接字
    close(sock);

    return 0;
}

演算結果:

サーバーを起動します

サーバーは監視状態です

クライアントを起動します。

この時点で、簡単なソケット通信コードが完成します

 

 

 

 

 

 

 

おすすめ

転載: blog.csdn.net/weixin_40179091/article/details/113024907