前言:
欢迎来到C/C++网络编程的世界,这次的目标就是学会封装client和server,如果对于C/C++网络编程没有任何基础,请看:TCP_Client_and_Server,C/C++基础实现_c++ tcp client-优快云博客
https://blog.youkuaiyun.com/Auli6/article/details/151973977?spm=1001.2014.3001.5501针对于client的封装已经发布了,请看:Client客户端class封装思路,C/C++-优快云博客
https://blog.youkuaiyun.com/Auli6/article/details/152171500?spm=1001.2014.3001.5501 这次的目标就是实现封装server服务器以及结合client客户端实现简易通信!
- server服务器封装思路
对于server服务器的封装,首先我们要了解的是,服务器是怎么进行接收消息并且实现通信的,大致是这样的通信流程:socket -> bind -> listen -> accept -> recv -> send;,了解过客户端的通信流程的相信已经看出来这里的不同了,相比之下,服务器的通信流程更加繁琐,别担心,我们一步一步来。
第一步,先创建一个头文件,暂时命名为socket_server.h
第二步,正式开始编码,我们将必要的头文件写上:

第三步,创建类,类名暂时起为,TCPserver,然后我们就要想了,public里面肯定是函数,private里面填什么?这里有个技巧,如果一个变量(或函数)在public中的函数中使用次数达到二次以上,那么就将这个变量(或函数)加入到private中,那么就清晰明了了,我们按照server的通信流程来说的话,那么肯定要socket这个函数啊,这个函数返回的值我们常用的很,不管是bind还是listen都需要这个值,通过这样的判断,我们可以很轻易的得知什么变量(或函数)必须加入到private中,这里额外说一下,我们必须要一个bool类型的connected,你想,这是个类,里面的函数只要是public都可以被调用,那么也就是说,我可以不按顺序调用函数,万一我的服务器还没搭建好,我直接调用send函数,是不是就不行啦,所以connected是必须的,其他的变量(或函数)也通过这样的方式判断下来,就可以知道private中到底有什么了。

第四步,按照流程将对应函数写入public中,好的我们回顾一下我们的流程,噢噢噢,第一步是创建socket套接字,那么我们就应该写一个函数来实现这个功能,为了方便,我们也可以将bind函数也写进这个函数中(看完文章后,可以试试把bind和socket函数分开),我们暂时把这个函数叫做set_server_base,这个函数的返回值类型是bool类型,因为我们要判断socket或者bind方法是否成功,函数如下:
bool set_server_base() {
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
cerr << "创建服务器失败!" << endl;
return false;
}
cout << "创建服务器成功!" << endl;
sockaddr_in server_addr;
const int port = 7777;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
socklen_t server_addr_len = sizeof(server_addr);
if (bind(server_fd, (sockaddr*)&server_addr, server_addr_len) < 0) {
cerr << "bind绑定失败!" << endl;
close(server_fd);
return false;
}
cout << "bind绑定成功!" << endl;
return true;
}
第五步,将服务器设置为监听状态,要用到的函数就是listen,那么函数名我们就暂时叫listen_server,这个函数依旧是bool类型,因为我们要判断监听模式是否设置成功,函数如下:
bool listen_server(const int& max_for_listen_member) {
if (listen(server_fd, max_for_listen_member) < 0) {
cerr << "监听设置失败!" << endl;
close(server_fd);
return false;
}
cout << "监听设置成功!" << endl;
return true;
}
第六步,接下来就简单了,根据步骤,就是要接收连接accept(这个函数很重要),接收消息recv,回复消息send,那么相对应的函数名我们可以起为accept_server,recv_server,send_server,函数如下:
bool accept_server() {
socklen_t client_addr_len = sizeof(client_addr);
server_to_client = accept(server_fd, (sockaddr*)&client_addr, &client_addr_len);
if (server_to_client < 0) {
cerr << "接收失败!" << endl;
return false;
}
cout << "accept成功!" << endl;
connected = true;
return true;
}
bool send_server(const string& data) {
if (connected == false) {
cerr << "未进行连接,无法进行传输数据!" << endl;
return false;
}
int bytes_send = send(server_to_client, data.c_str(), data.length(), 0);
if (bytes_send < 0) {
cerr << "发送失败!" << endl;
close(server_to_client);
return false;
}
cout << "发送成功!消息为:" << data << endl;
return true;
}
string recv_server() {
if (!connected) {
cerr << "未进行连接,不能使用接收消息功能!" << endl;
return "";
}
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
int bytes_recv = recv(server_to_client, buffer, sizeof(buffer) - 1, 0);
if (bytes_recv < 0) {
cerr << "接收消息失败!" << endl;
close_client();
return "";
}
else if (bytes_recv == 0) {
cout << "客户端主动断开连接!" << endl;
close_client();
return "";
}
buffer[bytes_recv] = '\0';
return string(buffer);
}
完成编码后,那么就可以使用这些函数进行服务器的搭建了,但是,我们的目标远远不止于此,我们要是想实现服务器不关闭,一直等待客户端的连接,那么就必须保证服务器一直开着,但是怎么保证?也就是server_fd > 0,但是这个是private中的值啊,外部无法访问,那么就需要一个函数来达到这样的作用,那么有了服务器的判断,客户端也必须要一个函数,对此,当我们用完服务器之后或者客户端之后,得要两个函数来完成关闭客户端和服务器的作用,那么综合起来,我们需要的函数如下:
bool is_connect() {
return connected;
}
bool is_server_life() {
return server_fd > 0;
}
void close_client() {
if (connected) {
cout << "正在关闭客户端!" << endl;
close(server_to_client);
connected = false;
}
}
void close_server() {
cout << "正在关闭服务器!" << endl;
close_client();
if (server_fd > 0) {
close(server_fd);
server_fd = -1;
cout << "服务器已经关闭!" << endl;
}
}
~TCPserver() {
close_server();
}
bool re_connected() {
cout << "正在等待新的客户端接入" << endl;
close_client();
return accept_server();
}
完成这一切设置之后,我们的server服务器封装完成,代表着我们可以使用这个头文件来创建关于这个类的对象,从而使用函数创建服务器!
- 封装的client和server的使用
至此,client和server的封装完成,相信你也自己跟着再次敲了一遍代码,那么现在,就要开始通信了,那么在刚开始的时候,我们要想清楚,这个是怎么开始通信的?首先,我们先看一下流程: 先进行server的socket和bind和listen -> client的connect连接server -> socket 开始accept接收 -> client 开始发送消息send -> server 接收消息 -> server回复消息 -> client 接收消息 -> 关闭通信通道 -> 关闭server -> end;
这个流程真的很麻烦,但是这就像打电话一样,你可以自己模拟一下这个过程,差不多就可以理解了,接下来附一张基本的运行程序代码:
#include "socket_client.h"
#include "socket_server.h"
int main() {
const int listen_max_count = 5;
TCPserver server;
server.set_server_base();
server.listen_server(listen_max_count);
TCPclient client("127.0.0.1", 7777);
client.connect_to_server();
server.accept_server();
client.send_data("hello");
cout << server.recv_server();
server.send_server("你好!");
cout << client.receive_data();
return 0;
}
如果没错的话,那么,结果应该是:

但是这是一个函数中运行的结果,就好像你自己给自己打电话,要想做成循环的话,那么就不能这样做,所以,我们的普遍做法是,将client和server分成两个不同的文件来实现这个功能。
用两个不同的文件来存放我们的头文件和main函数,来达到像这样的作用,我这里是用了两个编辑器,为了方便直观,你可以用同一个编辑器打开两个文件,这样也是可以的!
接下来我要补充几个要点,一些细节的处理,可能会帮到你,这些细节都是我当时犯的错误。
第一个:传输的消息是使用length(C++_string)还是strlen(C_ const char*)还是sizeof?关系到send和read(网络编程不常用)和recv,都有这个的疑问,首先需要我们理解的是这到底代表着什么?首先对于strlen(这里也同样适用于length,由于C在网络编程占比较多,故用strlen举例)代表的是从开头到结尾的字符数量,也就是一个字符串到底有多少个字符,第二个sizeof代表的是这个字符串一共能占多少字节内容,比如一个int的数组,里面存放着10个数,那么sizeof出来就是40个字节,比如char buffer[1024],这个就表示buffer最多占1024个字符(char类型占一个字节大小),但是末尾要用’\0’填充(C),好,我们的send函数要用strlen函数,因为send只是想发送有数据的内容,也就是只想要有字符的内容,所以用strlen来获取一个字符串里到底有多少个字符。recv要用sizeof函数,recv的主要工作就是接收消息,首先,我们不能精确指定我们要多少字节大小的字符串,我们只能设定一个范围,也就是说,我接受的字符串长度不能大于1024个字节,所以用的是sizeof函数用于计算最大容量,read和recv同理。
第二个:close的时机,也就是说,当我们发生什么情况的时候,使用close来关闭一些东西,我们先来看服务器,服务器的话,有两个文件描述符,一个是通过socket返回的套接字创建的一个服务器 server_fd = socket(AF_INET,SOCK_STREAM,0);第二个是通过accept函数返回的一个文件描述符,这个是服务器和客户端的专属连接通道,关闭这个就像等于关闭两者之间的通信通道,也就不能传输消息了,那么调用close的时机需要我们注意,假设说,一个客户端输入了bye,这个时候,如果服务器还想接收新的客户端的话,就需要关闭这个通信通道,然后break退出外层循环(这里是默认是服务器一直接收新的客户端),让服务器重新accept就可以,如果我们不想再接收新的客户端了,那么就需要关闭server_fd这个套接字,表示关闭服务器,不再接收新的客户端,但是这只是最简单的用法了,我们真正要讨论的是,当函数异常发生时,我们需要close什么?或者不close?我们来总结一下规律,当我创建了server_fd后,接下来就是bind,那么如果bind不成功,这个时候就需要close(server_fd),为什么?bind是你创建的信息和服务器的绑定,如果bind不成功,那么就相当于你自己的信息出错误了,需要自己手动调试,那么这个时候,就需要close(server_fd),如果不close,那么将会一直bind失败,listen也同理,关闭server_fd,accept需要注意,这个不需要关闭server_fd,因为我只是接收客户端失败了,说明,错误并不出在服务器的身上,那么就是客户端的问题了,服务器没必要因为客户端犯的错买单,所以不用关闭,其他的应该都知道了,你应该能总结出规律来,毕竟不是那么抽象,客户端也一样。
第三个,服务器不关闭,保持一直接收客户端的状态,但是while循环在哪里加?相信这个问题肯定是一个难点(之后可能不会了哈哈哈),我们来想一想,如果一个服务器永远不关闭,那么也就是说,while语句会一直执行下去,那么条件就是true,那么在何处添加另一个while循环呢?这里的统一用法就是在accept之后调用另一个while循环,然后继续操作,你可能会问,学习这个有什么用呢?说实话,像这种循环执行,更能体现出你对于代码的理解,你的循环添加处是对于整体的解释,并不是仅仅对于某一行代码或者说某一个函数的理解,你所看到的,是整个整体。
接下来是头文件的代码,供你参考:
socket_server.h:
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class TCPserver {
private:
int server_fd;
int server_to_client;
sockaddr_in client_addr;
bool connected;
public:
TCPserver() : server_fd(-1),connected(false) {};
bool set_server_base(){
server_fd = socket(AF_INET,SOCK_STREAM,0);
if(server_fd < 0){
cerr << "创建服务器失败!" << endl;
return false;
}
cout << "创建服务器成功!" << endl;
sockaddr_in server_addr;
const int port = 7777;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
socklen_t server_addr_len = sizeof(server_addr);
if(bind(server_fd,(sockaddr*)&server_addr,server_addr_len) < 0){
cerr << "bind绑定失败!" << endl;
close(server_fd);
return false;
}
cout << "bind绑定成功!" << endl;
return true;
}
bool listen_server(const int& max_for_listen_member){
if(listen(server_fd,max_for_listen_member) < 0){
cerr << "监听设置失败!" << endl;
close(server_fd);
return false;
}
cout << "监听设置成功!" << endl;
return true;
}
bool accept_server(){
socklen_t client_addr_len = sizeof(client_addr);
server_to_client = accept(server_fd,(sockaddr*)&client_addr,&client_addr_len);
if(server_to_client < 0){
cerr << "接收失败!" << endl;
return false;
}
cout << "accept成功!" << endl;
connected = true;
return true;
}
bool send_server(const string& data){
if(connected == false){
cerr << "未进行连接,无法进行传输数据!" << endl;
return false;
}
int bytes_send = send(server_to_client,data.c_str(),data.length(),0);
if(bytes_send < 0){
cerr << "发送失败!" << endl;
close(server_to_client);
return false;
}
cout << "发送成功!消息为:" << data << endl;
return true;
}
string recv_server(){
if(!connected){
cerr << "未进行连接,不能使用接收消息功能!" << endl;
return "";
}
char buffer[1024];
memset(buffer,0,sizeof(buffer));
int bytes_recv = recv(server_to_client,buffer,sizeof(buffer) - 1,0);
if(bytes_recv < 0){
cerr << "接收消息失败!" << endl;
close_client();
return "";
}else if(bytes_recv == 0){
cout << "客户端主动断开连接!" << endl;
close_client();
return "";
}
buffer[bytes_recv] = '\0';
return string(buffer);
}
bool is_connect(){
return connected;
}
bool is_server_life(){
return server_fd > 0;
}
void close_client(){
if(connected){
cout << "正在关闭客户端!" << endl;
close(server_to_client);
connected = false;
}
}
void close_server(){
cout << "正在关闭服务器!" << endl;
close_client();
if(server_fd > 0){
close(server_fd);
server_fd = -1;
cout << "服务器已经关闭!" << endl;
}
}
~TCPserver(){
close_server();
}
bool re_connected(){
cout << "正在等待新的客户端接入" << endl;
close_client();
return accept_server();
}
};
socket_client.h:
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <cstring>
using namespace std;
class TCPclient{
private:
int client_fd;
string server_ip;
int server_port;
bool connected;
public:
TCPclient(string server_ip_construct, int server_port_construct) : server_ip(server_ip_construct),server_port(server_port_construct),client_fd(-1),connected(false) {};
bool connect_to_server(){
client_fd = socket(AF_INET,SOCK_STREAM,0);
if(client_fd < 0){
cerr << "创建失败!" << endl;
return false;
}
cout << "客户端创建成功!" << endl;
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
if(inet_pton(AF_INET,server_ip.c_str(),&server_addr.sin_addr) < 0){
cerr << "地址转换失败,inet_pton方法失败!" << endl;
client_fd = -1;
return false;
}
cout << "地址已经转换成功!" << server_ip << ":" << server_port << endl;
cout << "正在连接服务器!" << endl;
if(connect(client_fd,(sockaddr*)&server_addr,sizeof(server_addr)) < 0){
cerr << "连接失败,client_fd转换为通信通道失败!" << endl;
close(client_fd);
client_fd = -1;
return false;
}
connected = true;
cout << "连接生效!请输入消息!" << endl;
return true;
}
bool send_data(const string& data){
if(!connected){
cerr << "连接未生效,无法发送消息!" << endl;
return false;
}
cout << "客户端发送的消息是: " << data << endl;
int bytes_send = send(client_fd,data.c_str(),data.length(),0);
if(bytes_send < 0){
cerr << "发送消息失败!" << endl;
connected = false;
return false;
}
return true;
}
string receive_data(){
if(!connected){
cerr << "未进行连接,无法接收服务器消息!" << endl;
return "";
}
char buffer[1024];
int bytes_receive = recv(client_fd,buffer,sizeof(buffer) - 1,0);
if(bytes_receive < 0){
cerr << "接收消息失败!" << endl;
connected = false;
return "";
}
buffer[bytes_receive] = '\0';
cout << "消息的字节大小是:" << bytes_receive << endl;
return string(buffer);
}
void disconnect(){
if(client_fd > 0){
cout << "正在断开与服务器的连接!" << endl;
close(client_fd);
client_fd = -1;
connected = false;
}
}
bool is_connect() const{
return connected;
}
bool re_connect(){
cout << "正在尝试重新进入!" << endl;
disconnect();
return connected;
}
~TCPclient(){
disconnect();
}
};
尾声
C/C++网络编程确实有点烧脑,但是就如同我们刚开始学C++那时,我们也什么都不懂,也是一步步走过来的,凭借着自己的理解,慢慢的就把C++学好了,如果你能学到这里,就能证明,你有着良好的C++基础以及编码能力,加油,未来的某一天,你会感谢今天努力的自己!
395

被折叠的 条评论
为什么被折叠?



