サーバーとクライアントはSocketを介して通信します
参照ブログ:
[TCPネットワークプログラミングにおけるconnect()、listen()、accept()の関係]->次の図は、このブログ(https://blog.csdn.net/tennysonsky/article/details/)からのものです。 45621341)
Webソケットプログラミングの基本API
サーバ:
- socket():ソケットを作成し、ソケットのIPアドレスタイプと送信プロトコルタイプを設定します。
- bind():IPアドレスとポート番号をソケットにバインドします。
- listen():ソケットをパッシブ接続リスニングソケットに変えます。リスニングソケットのポート番号。いつでもクライアントから接続要求を受信する準備ができています。
listen()関数はブロックしません。主なことは、Linuxカーネルにソケットと、ソケットに対応する接続キューの長さを通知することです。その後、listen()関数は終了します。 - accept():クライアントが正常に接続するまでブロックし、キュー内の完了した接続を削除します。
accept()関数は、確立された接続キューの先頭から完了した接続を取り出します。このキューに完了した接続がない場合、accept()関数は、キュー内の完了したクライアント接続が取り出されるまで、現在のスレッドをブロックします。 。 - read()\ write()、recv()\ send():クライアントとデータを交換します。
read()はブロッキングI / Oモードであり、完全なデータがカーネルバッファーで受信され、カーネルバッファーからプロセスにコピーされるまで、サーバープロセスはブロックされます。 - close():ソケットを閉じます。つまり、接続を閉じます。
クライアント:
- socket():ソケットを作成し、ソケットのIPアドレスタイプと送信プロトコルタイプを設定します。
クライアントは接続要求を送信するパーティであり、ポートを固定する必要はありませんが、ランダムに割り当てることができるため、バインドする必要はありません。 - connect():クライアントは、サーバーのIPアドレスとポート番号に従ってサーバーとの接続を要求します。
クライアントはサーバーにアクティブに接続し、接続はスリーウェイハンドシェイクを介して確立されます。この接続のプロセスは、この機能ではなくカーネルによって完了されます。この機能の機能は、LinuxカーネルにTCPスリーウェイハンドシェイク接続を自動的に完了するように通知することだけであり、最後に接続の結果をこの関数の戻り値に返します(接続が成功した場合は0、失敗した場合は-1)。 - read()\ write()、recv()\ send()
- 閉じる()
サーバーコード
主な機能は、クライアントから送信された文字列のすべての小文字を大文字に変換してから、クライアントに返すことです。(追記:私は学習に慣れていないので、コードのコメントはより詳細です。実際、コードの量はそれほど多くありません)リファレンスブログ:Linuxソケットプログラミング
#include <iostream>
#include <stdio.h>
#include <cstring> // void *memset(void *s, int ch, size_t n);
#include <sys/types.h> // 数据类型定义
#include <sys/socket.h> // 提供socket函数及数据结构sockaddr
#include <arpa/inet.h> // 提供IP地址转换函数,htonl()、htons()...
#include <netinet/in.h> // 定义数据结构sockaddr_in
#include <ctype.h> // 小写转大写
#include <unistd.h> // close()、read()、write()、recv()、send()...
using namespace std;
const int flag = 0; // 0表示读写处于阻塞模式
const int port = 8080;
const int buffer_size = 1<<20;
int main(int argc, const char* argv[]){
// 创建服务器监听的套接字。Linux下socket被处理为一种特殊的文件,返回一个文件描述符。
// int socket(int domain, int type, int protocol);
// domain设置为AF_INET/PF_INET,即表示使用ipv4地址(32位)和端口号(16位)的组合。
int server_sockfd = socket(PF_INET,SOCK_STREAM,0);
if(server_sockfd == -1){
close(server_sockfd);
perror("socket error!");
}
// /* Enable address reuse */
// int on = 1;
// int ret = setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
// 此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr)); // 结构体清零
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(port); // 端口16位, 此处不用htons()或者错用成htonl()会连接拒绝!!
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地所有IP
// 另一种写法, 假如是127.0.0.1
// inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr.s_addr);
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// bind()函数的主要作用是把ip地址和端口绑定到套接字(描述符)里面
// struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
// 一般情况下,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中。
if(bind(server_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
close(server_sockfd);
perror("bind error");
}
// 第二个参数为相应socket可以排队的准备道来的最大连接个数
if(listen(server_sockfd, 5) == -1){
close(server_sockfd);
perror("listen error");
}
printf("Listen on port %d\n", port);
while(1){
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// accept()函数从处于established状态的连接队列头部取出一个已经完成的连接,
// 如果这个队列没有已经完成的连接,accept()函数就会阻塞当前线程,直到取出队列中已完成的客户端连接为止。
int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
char ipbuf[128];
printf("client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(client_addr.sin_port));
// 实现客户端发送小写字符串给服务端,服务端将小写字符串转为大写返回给客户端
char buf[buffer_size];
while(1) {
// read data, 阻塞读取
int len = recv(client_sockfd, buf, sizeof(buf),flag);
if (len == -1) {
close(client_sockfd);
close(server_sockfd);
perror("read error");
}else if(len == 0){
// 这里以len为0表示当前处理请求的客户端断开连接
break;
}
printf("read buf = %s", buf);
// 小写转大写
for(int i=0; i<len; ++i) {
buf[i] = toupper(buf[i]);
}
printf("after buf = %s", buf);
// 大写串发给客户端
if(send(client_sockfd, buf, strlen(buf),flag) == -1){
close(client_sockfd);
close(server_sockfd);
perror("write error");
}
memset(buf,'\0',len); // 清空buf
}
close(client_sockfd);
}
close(server_sockfd);
return 0;
}
クライアントコード
// client 端相对简单, 另外可以使用nc命令连接->nc ip prot
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
const int port = 8080;
const int buffer_size = 1<<20;
int main(int argc, const char *argv[]) {
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (client_sockfd == -1) {
perror("socket error");
exit(-1);
}
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(port);
inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr.s_addr);
int ret = connect(client_sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
if (ret == -1) {
perror("connect error");
exit(-1);
}
while(1) {
char buf[buffer_size] = {
0};
fgets(buf, sizeof(buf), stdin); // 从终端读取字符串
write(client_sockfd, buf, strlen(buf));
//接收, 阻塞等待
int len = read(client_sockfd, buf, sizeof(buf));
if (len == -1) {
perror("read error");
exit(-1);
}
printf("client recv %s\n", buf);
}
close(client_sockfd);
return 0;
}
遭遇問題
connect error: Connection refused
、クライアントがサーバーに接続すると、接続が拒否されました。
理由:ホストバイトオーダーおよびネットワークバイトオーダー変換関数の使用が正しくないため、ポート変換が使用されhtonl()
ます。ポートが16ビットの場合は、htons()とntohs()を使用する必要があります。IPが32ビットの場合は、htonl()とntohl()を使用する必要があります。
(注:データストリームにはビッグエンディアンのバイト順序とリトルエンディアンのバイト順序があります。TCP/ IPプロトコルでは、ネットワークデータストリームがビッグエンドのバイト順序を採用することが規定されています。ビッグエンドのエンディアンのストレージ原理を分析すると、charデータの場合は次のことがわかります。 1バイトしか占有しないため、この問題は発生しません。これが、データバッファが一般にcharタイプとして定義されている理由の1つです。IPアドレスやポート番号などのchar以外のデータの場合、データをネットワークに送信する必要があります。アップロードする前にビッグエンディアンモードに変換し、データを受信した後、受信ホストと一致するストレージモードに変換します。)
// Linux 系统为主机字节序和网络字节序的转换提供了4个函数
#include <arpa/inet.h>
/*主机字节顺序 --> 网络字节顺序*/
uint32_t htonl(uint32_t hostlong);/* IP */
uint16_t htons(uint16_t hostshort);/* 端口 */
/*网络字节顺序 --> 主机字节顺序*/
uint32_t ntohl(uint32_t netlong);/* IP */
uint16_t ntohs(uint16_t netshort);/* 端口 */
bind error: Address already in use
、バインドされたアドレス(ip + port)はすでに使用されています。
理由:Ctrl + Zはタスクの実行を中断しますが、タスクは終了しておらず、プロセスで一時停止状態を維持しているだけです。(Ctrl + Cは、プログラムの実行を強制的に終了し、プロセスを終了します)を
使用してnetstat -anp | grep 8080
、サーバーのリスニングポート8080のステータスを表示します。pid 4600のサーバープロセスと4623のクライアントプロセスはまだESTABLISHED
状態にあり、終了していないことがわかります。kill -9 pid
エンドクライアントとサーバプロセス、ポートを解放します。