1: ソケットとは何ですか?
ソケットを初めて使用する学生は、ソケットの中国語名である「ソケット」も知っておく必要があります。中国語名というよりも、それが何なのかを知っておく必要があります。中国語名の実際の意味は気にせずに、まずソケットとは何かを理解しましょう。
オンラインで生成されるデータは、プロトコル スタックを通じてレイヤーごとにカプセル化され、ネットワーク カードを通じてネットワークに送信され、ネットワークを通じてサーバーに送信され、サーバーはレイヤーごとにカプセル化を解除して必要なデータを取得します。
プロトコル スタックはオペレーティング システムに統合されています。TCP、UDP、その他のプロトコルがどのように実装されているかを気にする必要はありません。重要なのは、アプリケーションのデータが正常に送信され、サーバーから受信できるかどうかです。 .データ。これにはブリッジが必要で、一方の端はオペレーティング システムのプロトコル スタックに接続され、もう一方の端はユーザーのアプリケーション データに接続されます。ソケットはこのブリッジです。
それでは、中国語のソケットという名前をもう一度理解しましょう。調べてみた結果、私が最も同意する説明は、「ソケットとはソケットパイプ、つまり2つの水道管を接続するパイプを指し、「単語」はこの接続のデータです」です。識別子は WORD であるため、ソケットは接続を識別するデータ本体です。
WORD とは何なのかについて質問する生徒もいます。Linux やその他のシステムでは、「ソケット」は「ソケット ワード」に対応するため、「ワード」も「ワード」に対応します。この「ワード」は、格納されているソケットのデータ識別子を指す場合があります。ポート番号は 2 バイトのWORDであるためです。
下の図は非常に具体的であり、上の図ほど抽象的ではありません。
ソケットの説明はこれで終わりです。
2:ソケット通信処理
TCP 接続プロセスと同様に、TCP リンクの確立には 3 回のハンドシェイクが必要で、TCP リンクの削除には 4 回のウェーブが必要です。ソケット通信にも独自の一連のプロセスがあります。
クライアントの場合:
1. 通信用のソケット(fd)を作成する
2. サーバーに接続するには、接続するサーバーの IP とポートを指定する必要があります。
3. 接続が正常に確立され、クライアントとサーバーは接続チャネルを確立します。
1>データを送信できます
2>データを受信できます
4. 通信終了、切断
サーバーの場合:
1. リッスン用のソケットを作成する
1>リッスン: クライアント接続をリッスンします。
2>ソケット: このソケットは実際にはファイル記述子です
2. このリスニング ファイル記述子をローカル IP とポートにバインドします (IP とポートはサーバーのアドレス情報です)。
1>クライアントはサーバーに接続するときにこの IP とポートを使用します
3. 監視を設定すると、監視 fd が動作を開始します。
4. ブロックと待機: クライアントが接続を開始すると、ブロックを解除してクライアントの接続を受け入れ、クライアントと通信するためのソケット (fd) を取得します。
5. サーバーとクライアント間の通信
1>データを受信する
2>データを送信する
6. 通信終了、切断
その3:ソケット通信機能の詳細説明
1.socket()関数
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
注: 上記のタイプとプロトコルは自由に組み合わせることができません (たとえば、SOCK_STREAM と IPPROTO_UDP を組み合わせることはできません)。プロトコルが 0 の場合、タイプ type に対応するデフォルトのプロトコルが自動的に選択されます。
ソケットを作成するためにソケットを呼び出すと、返されるソケット記述子はプロトコル ファミリ (アドレス ファミリ、AF_XXX) 空間に存在しますが、特定のアドレスを持ちません。アドレスを割り当てたい場合は、bind() 関数を呼び出す必要があります。そうしないと、connect() または listen() を呼び出すときにシステムが自動的にポートをランダムに割り当てます。
2.bind()関数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
bind() 関数は、アドレス ファミリ内の特定のアドレスをソケットに割り当てます。たとえば、AF_INET および AF_INET6 に対応して、ipv4 または ipv6 アドレスとポート番号の組み合わせがソケットに割り当てられます。
3. listen()関数
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
サーバーはクライアントからのデータがあるかどうかを常に監視する必要があるため、サーバーは listen() を呼び出して確立されたソケットをリッスンし、クライアントが connect() を呼び出して接続要求を発行すると、サーバーは要求を受け取ります。
4. connect()関数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
クライアントは connect 関数を呼び出して TCP サーバーとの接続を確立します。
5. accept()関数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
socket()、bind()、および listen() を呼び出した後、サーバー側は指定されたソケット アドレスをリッスンします。クライアントは、socket() と connect() を呼び出した後、接続チャネルを確立してサーバーにリクエストを送信し、サーバーがリクエストを監視した後、accept() 関数を呼び出してリクエストを受信します。その後、通常のファイルの読み取りおよび書き込み I/O 操作と同様のネットワーク I/O 操作を開始できます。
6. read()、write()関数
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
read 関数は、fd からコンテンツを読み取る役割を果たします。読み取りが成功すると、read は実際に読み取られたバイト数を返します。戻り値が 0 の場合は、ファイルの終わりが読み取られたことを意味します。それが未満の場合は、 0、エラーが発生したことを意味します。エラーが EINTR の場合は、読み取りが割り込みによって行われたことを意味し、ECONNREST の場合は、ネットワーク接続に問題があることを意味します。
write 関数は、buf 内の nbytes バイトをファイル記述子 fd に書き込み、成功すると、書き込まれたバイト数を返します。失敗すると、-1 が返され、errno 変数が設定されます。ネットワーク プログラムでは、ソケット ファイル記述子に書き込むときに 2 つの可能性があります。1) write の戻り値は 0 より大きく、データの一部またはすべてが書き込まれたことを示します。2) 戻り値が 0 未満であり、エラーが発生しました。エラーの種類に応じて対処する必要があります。エラーが EINTR の場合は、書き込み中に割り込みエラーが発生したことを意味します。EPIPE の場合は、ネットワーク接続に問題がある (相手が接続を閉じている) ことを意味します。
ネットワーク I/O 操作は、read()/write() 関数だけでなく、次のグループも含みます。
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
その声明は次のとおりです。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
7. close()関数
int close(int fd);
ソケット接続を閉じると、すぐに呼び出し元のプロセスに戻ります。この記述子は呼び出しプロセスでは使用できなくなります。つまり、読み取りまたは書き込みの最初のパラメーターとして使用できなくなります。
注: クローズ操作では、対応するソケット記述子の参照カウントが -1 だけ減少します。参照カウントが 0 の場合にのみ、TCP クライアントはサーバーに終了要求を送信するようにトリガーされます。
4: ソケット通信の練習
クライアント:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#define SERVER_PORT 8888
int main()
{
//客户端只需要一个套接字文件描述符,用于和服务器通信
int serverSocket;
//描述服务器的socket
struct sockaddr_in serverAddr;
char sendbuf[200]; //存储 发送的信息
char recvbuf[200]; //存储 接收到的信息
int iDataNum;
/*********************************************************************/
/* 1-创建客户端套接字 */
/*********************************************************************/
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
//指定服务器端的ip,本地测试:127.0.0.1
//inet_addr()函数,将点分十进制IP转换成网络字节序IP
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/*********************************************************************/
/* 2-连接服务端 */
/*********************************************************************/
if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
perror("connect");
return 1;
}
printf("连接到主机...\n");
/*********************************************************************/
/* 3-发送接收消息 */
/*********************************************************************/
while(1)
{
printf("发送消息:");
scanf("%s", sendbuf);
printf("\n");
send(serverSocket, sendbuf, strlen(sendbuf), 0); //向服务端发送消息
if(strcmp(sendbuf, "quit") == 0) break;
printf("读取消息:");
recvbuf[0] = '\0';
iDataNum = recv(serverSocket, recvbuf, 200, 0); //接收服务端发来的消息
recvbuf[iDataNum] = '\0';
printf("%s\n", recvbuf);
}
close(serverSocket);
return 0;
}
サーバ:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#define SERVER_PORT 8888
/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如close后,断开与客户端的连接
*/
int main()
{
//调用socket函数返回的文件描述符
int serverSocket;
//声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器
struct sockaddr_in server_addr;
struct sockaddr_in clientAddr;
int addr_len = sizeof(clientAddr);
int clientSocket;
char buffer[200]; //存储 发送和接收的信息
int iDataNum;
/*********************************************************************/
/* 1-创建服务端套接字 */
/*********************************************************************/
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}
memset(&server_addr,0, sizeof(server_addr));
//初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)
//bind三个参数:服务器端的套接字的文件描述符
/*********************************************************************/
/* 2-服务端绑定监听的IP和por */
/*********************************************************************/
if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("connect");
return 1;
}
/*********************************************************************/
/* 3-服务端开始监听 */
/*********************************************************************/
if(listen(serverSocket, 5) < 0)//开启监听 ,第二个参数是最大监听数
{
perror("listen");
return 1;
}
/*********************************************************************/
/* 4-接收发送消息 */
/*********************************************************************/
printf("监听端口: %d\n", SERVER_PORT);
//调用accept函数后,会进入阻塞状态
//accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,
//serverSocket和client。
//serverSocket仍然继续在监听状态,client则负责接收和发送数据
//clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号
//addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。
//传出的是客户端地址结构体的实际长度。
//出错返回-1
clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len);
if(clientSocket < 0)
{
perror("accept");
}
printf("等待消息...\n");
//inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP
//表达式:char *inet_ntoa (struct in_addr);
printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来
printf("Port is %d\n", htons(clientAddr.sin_port));
while(1)
{
buffer[0] = '\0';
iDataNum = recv(clientSocket, buffer, 1024, 0);
if(iDataNum < 0)
{
continue;
}
buffer[iDataNum] = '\0';
if(strcmp(buffer, "quit") == 0) break;
printf("收到消息: %s\n", buffer);
printf("发送消息:");
scanf("%s", buffer);
send(clientSocket, buffer, strlen(buffer), 0); //服务端也向客户端发送消息
if(strcmp(buffer, "quit") == 0) break; //输入quit停止服务端程序
}
close(clientSocket);
close(serverSocket);
return 0;
}
Linux 上で gcc を使用してコンパイルします。
gcc server.c -o server
gcc client.c -o client
まずサーバーを実行します。
クライアントを再度実行します。
クライアント サーバーはメッセージを送受信します。