🌞前言
在这篇文章里我们会实现一个小项目:客服端通过socket与服务器建立连接,并且发送消息。项目详情如下:
- 编程语言:C++
- 运行环境:Ubuntu
- 项目技术:socket
🌞一、基本概念
网络通讯是指两台计算机中的程序进行传输消息的过程。
- 客户端∶指主动发起通讯的程序。
客户端必须提前知道服务端的IP地址和通讯端口。- 服务端∶指被动的等待,然后为向它发起通讯的客户端提供服务。
服务端不需要知道客户端的IP地址。
🌞二、第一个网络通讯程序
🌼2.1 网络通讯的流程示意图
🌼2.2 程序模块
客户端client.cpp
/* * 程序名:client.cpp,此程序用于演示socket的客户端 */ #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; int main(int argc,char *argv[]) { if (argc!=3) { cout << "Using:./client 服务端的IP 服务端的端口\nExample:./client 192.168.101.139 5005\n\n"; return -1; } /* 第1步:创建客户端的socket。 */ int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd==-1) { perror("socket"); return -1; } /* 第2步:向服务器发起连接请求。*/ struct hostent* h; // 用于存放服务端IP的结构体。 if ( (h = gethostbyname(argv[1])) == 0 ) // 把字符串格式的IP转换成结构体。 { cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1; } struct sockaddr_in servaddr; // 用于存放服务端IP和端口的结构体。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // 指定服务端的IP地址。 servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。 if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) // 向服务端发起连接清求。 { perror("connect"); close(sockfd); return -1; } /* 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。*/ char buffer[1024]; for (int ii=0;ii<3;ii++) // 循环3次,将与服务端进行三次通讯。 { int iret; memset(buffer,0,sizeof(buffer)); sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。 // 向服务端发送请求报文。 if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; } cout << "发送:" << buffer << endl; memset(buffer,0,sizeof(buffer)); /* 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。*/ if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) { cout << "iret=" << iret << endl; break; } cout << "接收:" << buffer << endl; sleep(1); } /* 第4步:关闭socket,释放资源。*/ close(sockfd); }
服务端server.cpp
/* * 程序名:server.cpp,此程序用于演示socket通信的服务端 */ #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; int main(int argc,char *argv[]) { if (argc!=2) { cout << "Using:./server 通讯端口\nExample:./server 5005\n\n"; // 端口大于1024,不与其它的重复。 cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n"; cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n"; return -1; } // 第1步:创建服务端的socket。 int listenfd = socket(AF_INET,SOCK_STREAM,0); if (listenfd==-1) { perror("socket"); return -1; } // 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放服务端IP和端口的数据结构。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // 指定协议。 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。 servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,普通用户只能用1024以上的端口。 // 绑定服务端的IP和端口。 if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { perror("bind"); close(listenfd); return -1; } // 第3步:把socket设置为可连接(监听)的状态。 if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; } // 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。 int clientfd=accept(listenfd,0,0); if (clientfd==-1) { perror("accept"); close(listenfd); return -1; } cout << "客户端已连接。\n"; // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。 char buffer[1024]; while (true) { int iret; memset(buffer,0,sizeof(buffer)); // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。 // 如果客户端已断开连接,recv()函数将返回0。 if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) { cout << "iret=" << iret << endl; break; } cout << "接收:" << buffer << endl; strcpy(buffer,"ok"); // 生成回应报文内容。 // 向客户端发送回应报文。 if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; } cout << "发送:" << buffer << endl; } // 第6步:关闭socket,释放资源。 close(listenfd); // 关闭服务端用于监听的socket。 close(clientfd); // 关闭客户端连上来的socket。 }
🌞三、运行测试
小伙伴可以在虚拟机或者购买服务器上运行喔,要求ubuntu环境。可以连接到同一个服务器,也可以连接到多个不同的服务器,这里我们指定一个为客户端client,一个为服务端server。
🌻3.1 准备工作
将上面两个cpp文件创建完成,显示如下:
使用xshell两次连接到同一个服务器【有多个服务器也可以让xshell连接到不同的服务器】,这里我们指定一个为客户端client,一个为服务端server。
🌻3.2 编译
客户端:
//编译client.cpp命令 g++ -g -o client client.cpp
服务端:
//编译server.cpp命令 g++ -g -o server server.cpp
🌻3.3 运行
查看服务器的IP地址
ip addr show
IP地址显示是172.23.23.16
服务端
打开服务端的连接,让服务器处于监听状态(ctrl+c可以退出):
//2003是端口号 ./server 2003
客户端
再打开客户端的连接,让其访问服务端+建立连接:
//172.23.23.16是IP地址,2003是端口号 ./client 172.23.23.16 2003
查看服务端内容:
🌞四、程序分析
🌷4.1 客户端程序
client.cpp是一个简单的客户端程序,用于与服务器进行通讯。让我们分步来看:
命令行参数检查:
if (argc!=3) { cout << "Using:./client 服务端的IP 服务端的端口\nExample:./client 192.168.101.139 5005\n\n"; return -1; }
这段代码是在程序开始时对命令行参数进行检查。程序预期接收两个参数:服务端的IP地址和端口号。
argc
表示命令行参数的数量,argv
是一个指向参数数组的指针。
argc != 3
:检查参数数量是否等于3,如果不等于3,说明用户没有提供正确的参数数量。这里执行客户端命令用的是./client 172.23.23.16 2003。参数分别是:
./client
:表示程序名称。172.23.23.16
:表示服务端的IP地址。2003
:表示服务端的通讯端口。
1.创建客户端socket:
/* 第1步:创建客户端的socket。 */ int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd==-1) { perror("socket"); return -1; }
这段程序的作用是创建客户端的套接字(socket),并进行创建的错误检查。程序分析:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
这行代码创建了一个套接字,其中:
AF_INET
指定了套接字的地址族为IPv4。SOCK_STREAM
指定了套接字的类型为流式套接字,即TCP套接字。0
表示使用默认的协议。
if (sockfd == -1)
这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket()
函数返回-1
,程序通过perror("socket")
输出相关错误信息,然后返回-1
表示程序执行失败。
2.将服务端发送连接请求:
/* 第2步:向服务器发起连接请求。*/ struct hostent* h; // 用于存放服务端IP的结构体。 if ( (h = gethostbyname(argv[1])) == 0 ) // 把字符串格式的IP转换成结构体。 { cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1; } struct sockaddr_in servaddr; // 用于存放服务端IP和端口的结构体。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // 指定服务端的IP地址。 servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。 if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) // 向服务端发起连接清求。 { perror("connect"); close(sockfd); return -1; }
这段程序的作用是向服务器发起连接请求。程序分析:
struct hostent* h;
声明了一个指向hostent
结构体的指针,用于存放服务端的IP地址信息。if ((h = gethostbyname(argv[1])) == 0)
调用gethostbyname()
函数,把服务端IP地址从字符串类型转换成hostent
结构体。如果转换失败(返回值为0),则输出错误信息并关闭套接字sockfd
,然后返回 -1。struct sockaddr_in servaddr;
声明sockaddr_in
结构体变量servaddr
,用于存放服务端的IP地址和端口号信息。memset(&servaddr, 0, sizeof(servaddr));
使用memset()
函数将servaddr
结构体清零,以便后续使用。servaddr.sin_family = AF_INET;
设置servaddr
结构体中的地址族为 IPv4。memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
将通过gethostbyname()
函数获取的服务端IP地址复制到servaddr
结构体中的sin_addr
字段。servaddr.sin_port = htons(atoi(argv[2]));
将命令行参数中的服务端通信端口号转换成整数并转换成网络字节序(大端序),然后存放在servaddr
结构体中的sin_port
字段。if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
使用connect()
函数向服务端发起连接请求。如果连接失败,则输出错误信息,关闭套接字sockfd
,然后返回 -1。
3.与服务端通讯:
/* 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复, 收到回复后,再发下一个请求报文。*/ char buffer[1024]; for (int ii=0;ii<3;ii++) // 循环3次,将与服务端进行三次通讯。 { int iret; memset(buffer,0,sizeof(buffer)); sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。 // 向服务端发送请求报文。 if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; } cout << "发送:" << buffer << endl; memset(buffer,0,sizeof(buffer)); /* 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。*/ if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) { cout << "iret=" << iret << endl; break; } cout << "接收:" << buffer << endl; sleep(1); }
通过一个循环,发送三次请求报文给服务端,每次发送后等待服务端的回复。发送和接收都使用
send()
和recv()
函数,如果发送或接收失败则跳出循环。这段程序是用来与服务端进行通讯的,具体作用如下:
char buffer[1024];
声明一个长度为1024的字符数组buffer
,用来存储通讯的数据。for (int ii = 0; ii < 3; ii++)
循环3次,每次循环代表与服务端进行一次通讯。memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以确保没有之前的残留数据。sprintf(buffer, "这是第%d个超级女生,编号%03d。", ii + 1, ii + 1);
使用sprintf()
函数将字符串格式化写入buffer
数组,形成一个请求报文的内容。if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
通过send()
函数将请求报文发送给服务端,如果发送失败则输出错误信息,并跳出循环。cout << "发送:" << buffer << endl;
输出发送的请求报文内容。memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以便接收服务端的回应。if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
通过recv()
函数接收服务端的回应报文,如果接收失败或接收到的字节数为0则跳出循环。cout << "接收:" << buffer << endl;
输出接收到的服务端的回应报文内容。sleep(1);
程序暂停1秒钟,然后进行下一次通讯。
4.关闭socket:
/* 第4步:关闭socket,释放资源。*/ close(sockfd);
close()
函数用于关闭客户端套接字,释放资源。
🌷4.2 服务端程序
server.cpp是一个简单的服务端程序,用于与服务器进行通讯。让我们分步来看:
命令行参数检查:
if (argc!=2) { cout << "Using:./server 通讯端口\nExample:./server 5005\n\n"; // 端口大于1024,不与其它的重复。 cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n"; cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n"; return -1; }
首先,程序检查命令行参数数量是否为2,如果不是,则输出使用说明并退出程序。
在运行命令
./server 2003
时,命令行参数如下:
./server
:表示程序名称。2003
:表示通讯端口,即服务端程序将在2003端口上进行通讯。
1.创建服务端的socket:
// 第1步:创建服务端的socket。 int listenfd = socket(AF_INET,SOCK_STREAM,0); if (listenfd==-1) { perror("socket"); return -1; }
这段代码的作用是创建服务端的套接字(socket),并进行创建的错误检查。
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
这行代码创建了一个套接字,其中:
AF_INET
指定了套接字的地址族为IPv4。SOCK_STREAM
指定了套接字的类型为流式套接字,即TCP套接字,它提供了可靠的、双向的、基于连接的字节流。0
表示使用默认的协议。
if (listenfd == -1)
这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket()
函数返回-1
,程序通过perror("socket")
输出相关错误信息,然后返回-1
表示程序执行失败。
2.绑定IP地址和端口:
// 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放服务端IP和端口的数据结构。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // 指定协议。 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。 servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,普通用户只能用1024以上的端口。 // 绑定服务端的IP和端口。 if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { perror("bind"); close(listenfd); return -1; }
这段代码的作用是将服务端用于通信的IP地址和端口绑定到先前创建的套接字上。让我们逐步解析:
struct sockaddr_in servaddr;
声明一个sockaddr_in
结构体变量servaddr
,用于存放服务端IP地址和端口号的信息。memset(&servaddr, 0, sizeof(servaddr));
使用memset()
函数将servaddr
结构体清零,以确保没有之前的残留数据。servaddr.sin_family = AF_INET;
设置servaddr
结构体中的地址族为 IPv4。servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
将servaddr
结构体中的IP地址设置为INADDR_ANY
,表示服务端可以使用任意可用的网络接口。servaddr.sin_port = htons(atoi(argv[1]));
将命令行参数中的通信端口号转换成整数并转换成网络字节序(大端序),然后存放在servaddr
结构体中的sin_port
字段。if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0 )
通过bind()
函数将服务端IP地址和端口号与套接字进行绑定。如果绑定失败,输出错误信息,关闭套接字listenfd
,然后返回 -1 表示程序执行失败。
3.设置监听状态:
// 第3步:把socket设置为可连接(监听)的状态。 if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
这段代码的作用是将套接字设置为可连接(监听)的状态,以便服务端可以接受客户端的连接请求。
listen(listenfd, 5)
调用listen()
函数将套接字listenfd
设置为监听状态,并指定了可以同时处理的连接请求的最大数量为5。这意味着在任何时刻,服务器最多可以处理5个等待连接的客户端请求。if (listen(listenfd, 5) != 0 )
检查listen()
函数是否执行成功,如果不成功,输出错误信息,关闭套接字listenfd
,然后返回 -1 表示程序执行失败。设置套接字为监听状态后,服务端将能够接受客户端的连接请求,并把这些请求放入队列中等待处理。
4.接受客户端连接请求:
// 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。 int clientfd=accept(listenfd,0,0); if (clientfd==-1) { perror("accept"); close(listenfd); return -1; } cout << "客户端已连接。\n";
使用
accept()
函数受理客户端的连接请求,如果没有客户端连接上来,该函数将阻塞等待。这段代码的作用是接受客户端的连接请求。让我们逐步解释:
int clientfd = accept(listenfd, 0, 0);
调用accept()
函数接受客户端的连接请求。参数listenfd
是之前通过socket()
和bind()
函数创建并绑定的监听套接字。函数会在有客户端连接请求到达时返回一个新的套接字clientfd
,用于与该客户端进行通信。if (clientfd == -1)
检查accept()
函数是否成功接受客户端连接。如果返回值为-1
,表示发生了错误,通常是由于连接请求被拒绝或者出现了其他错误。在这种情况下,程序会输出错误信息,关闭监听套接字listenfd
,然后返回 -1 表示程序执行失败。cout << "客户端已连接。\n";
如果accept()
成功接受了客户端的连接请求,程序会输出一条消息表示客户端已经连接上了。综上所述,这段代码的作用是接受客户端的连接请求,并在成功连接时输出一条提示消息。
6.与客户端通信:
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。 char buffer[1024]; while (true) { int iret; memset(buffer,0,sizeof(buffer)); // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。 // 如果客户端已断开连接,recv()函数将返回0。 if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) { cout << "iret=" << iret << endl; break; } cout << "接收:" << buffer << endl; strcpy(buffer,"ok"); // 生成回应报文内容。 // 向客户端发送回应报文。 if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; } cout << "发送:" << buffer << endl; }
在一个循环中,不断接收客户端发送的请求报文,并回复 "ok"。
- 使用
recv()
函数接收客户端的请求报文,如果接收失败或客户端断开连接,则跳出循环。- 使用
send()
函数向客户端发送回应报文。这段代码是服务端与客户端进行通信的部分:
char buffer[1024];
声明一个长度为1024的字符数组buffer
,用于存储通讯的数据。while (true)
进入一个无限循环,持续与客户端进行通信,直到客户端断开连接或发生错误。memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以确保没有之前的残留数据。if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
通过recv()
函数接收客户端发送的请求报文,如果接收失败或客户端断开连接,则跳出循环。iret
变量存储接收到的字节数。cout << "接收:" << buffer << endl;
输出接收到的客户端请求报文内容。strcpy(buffer, "ok");
将回应报文内容设置为 "ok",表示服务端已接收到客户端的请求。if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
通过send()
函数向客户端发送回应报文,如果发送失败,则输出错误信息,并跳出循环。cout << "发送:" << buffer << endl;
输出发送的回应报文内容。总体来说,这段代码实现了一个服务端与客户端的简单通信过程。服务端不断接收客户端发送的请求报文,然后回复 "ok" 表示已收到。
7.关闭socket,释放资源:
// 第6步:关闭socket,释放资源。 close(listenfd); // 关闭服务端用于监听的socket。 close(clientfd); // 关闭客户端连上来的socket。
这段代码的作用是关闭套接字并释放相关资源
close(listenfd);
关闭服务端用于监听客户端连接请求的套接字listenfd
。一旦服务端不再需要监听新的连接请求,可以关闭这个套接字,以释放相关资源并告知操作系统不再维护该套接字的状态信息。close(clientfd);
关闭客户端连接的套接字clientfd
。一旦服务端与客户端的通信结束,可以关闭这个套接字,释放相关资源,并结束与该客户端的通信。通过关闭套接字,程序能够清理掉所占用的系统资源,并确保程序的正常结束。