Socket编程TCP
- 1、V1——EchoServer单进程版
- 2、V2——EchoServer多进程版
- 3、V3——EchoServer多线程版
- 4、V4——EchoServer线程池版
- 5、V5——多线程远程命令执行
- 6、验证TCP——Windows作为client访问Linux
- 7、connect的断线重连
1、V1——EchoServer单进程版
在TcpServer.hpp中实现服务器逻辑,然后TcpServer.cc中启动,客户端我们就不封装了,直接在TcpClient.cc中实现。Common.hpp和日志都是直接写过的,直接拿过来用。
下面先写出基本框架:
#pragma once#include <iostream>
#include <memory>const static uint16_t gport = 8080;class TcpServer
{
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){}void Start(){}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
需要保存sockfd,然后还需要端口号,同时使用bool类型的变量来表示服务端是否运行,通过Stop函数可以停止。
1、创建套接字socket
使用socket函数创建套接字,第一个参数domain表示域或协议家族,使用AF_INET表示网络通信,使用AF_UNIX表示本地通信,我们设置为AF_INET。第二个参数表示套接字类型,在UDP我们使用SOCK_DGRAM表示数据报,在TCP这里我们使用SOCK_STREAM,表示面向字节流。第三个参数设置为0即可。
socket成功返回文件描述符,失败返回-1错误码被设置。
#pragma once#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static uint16_t gport = 8080;class TcpServer
{
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;}void Start(){}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
2、填充网络信息并绑定。
bind函数将创建套接字返回的sockfd和传入的网络信息结构体对象绑定。由于UDP介绍过,不再赘述。
成功返回0,失败返回-1错误码被设置。
#pragma once#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static uint16_t gport = 8080;class TcpServer
{
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;// 2.填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); if (n < 0){LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);Die(BIND_ERR);}}void Start(){}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
3、将套接字设置为监听状态。
由于TCP是面向连接的,所以要求TCP要随时随地的等待被连接,因此需要将socket设置为监听状态。
第一个参数表示要监听的套接字,就是上面socket的返回值。第二参数表示全连接数量,我们设置为8即可,这个等后面讲TCP原理再说。
成功返回0,失败返回-1,错误码设置。
#pragma once#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"#define BACKLOG 8using namespace LogModule;const static uint16_t gport = 8080;class TcpServer
{
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;// 2.填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); if (n < 0){LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;// 3.设置套接字为监听状态n = ::listen(_listensockfd, BACKLOG);if (n < 0){LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;}void Start(){}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
4、获取新连接,接收客户端发送的数据
下面我们在Start函数中实现服务器获取客户端连接,并将客户端发送过来的字符串添加echo#发送回去。
使用accept函数来获取连接,第一个参数sockfd就是前面socket的返回值,第二个参数和第三个参数相当于输出型参数,因为我们需要知道是谁跟服务器建立了连接。
成功该函数返回一个文件描述符,失败返回-1,错误码被设置。
那么为什么socket已经返回一个文件描述符了,这个accept又返回一个文件描述符呢?——这是因为socket返回文件描述符是专门用来获取新连接的,而accept返回值是用来提供服务的。
另外,如果没有人连接,那么就会阻塞在accept这里。
#pragma once#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"#define BACKLOG 8using namespace LogModule;const static uint16_t gport = 8080;class TcpServer
{
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;// 2.填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); if (n < 0){LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;// 3.设置套接字为监听状态n = ::listen(_listensockfd, BACKLOG);if (n < 0){LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;}void HandlerRequest(int sockfd){LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while (true){int n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){inbuffer[n] = 0;std::string echo_string = "echo# ";echo_string += inbuffer;::write(sockfd, echo_string.c_str(), echo_string.size());}}}void Start(){_isrunning = true;while (_isrunning){LOG(LogLevel::DEBUG) << "accept ing...";struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensockfd, CONV(&peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;} LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << "[" << addr.Addr() << "]";HandlerRequest(sockfd);}}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
如上,我们引入UDP写的InetAddr.hpp,获取新连接后将客户端信息打印出来。同时将返回的sockfd传给HandlerRequest函数处理,在HandlerRequest我们读取客户端发送的数据,添加echo#发送回客户端。
这里读写数据可以直接使用read和write,因为TCP是面向字节流的。
接着实现一下TcpServer.cc,然后进行一下测试:
// TcpServer.cc
#include "TcpServer.hpp"int main()
{std::unique_ptr<TcpServer> svr_uptr = std::make_unique<TcpServer>();svr_uptr->InitServer();svr_uptr->Start();return 0;
}
编译后进行测试:
使用netstat -tlnp查看tcp服务,t表示tcp,l表示之查看listen状态的,n表示将能显示数字的都显示成数字,p表示显示最后一列PID/Program name。
可以看到我们服务启动起来了,端口号8080,并且当前状态处于监听状态。
5、使用telnet进行测试
使用telnet访问百度80端口,连接后输入ctrl ],然后回车,输入GET / HTTP/1.1回车再回车,可以获取百度的网页信息。
下面我们使用telnet测试我们写的客户端:
我们也可以通过浏览器,输入IP:端口号也可以:
6、实现客户端
客户端也是创建套接字,但是并不需要主动bind,由于TCP是面向连接的,所以客户端需要使用connect来和服务器建立连接,第一个参数就是socket的返回值,第二个参数就是服务端的信息。connect底层会自动进行bind。
成功返回0,失败返回-1,错误码被设置。
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>// ./client_tcp serverip serverport
int main(int argc, char* argv[])
{if (argc != 3){std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]); int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "create socket failed" << std::endl;return 2;} struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = ::htons(serverport);server.sin_addr.s_addr = ::inet_addr(serverip.c_str());int n = ::connect(sockfd, (const struct sockaddr*)&server, sizeof(server));if (n < 0){std::cout << "connect failed" << std::endl;return 3;}while (1){std::cout << "Please Enter@ ";std::string message;std::getline(std::cin, message);n = ::write(sockfd, message.c_str(), message.size());if (n > 0){char buffer[4096];int m = ::read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else break;}else break;}return 0;
}
接着使用我们写的客户端进行测试:
我们可以看到,客户端发送消息能够接受服务器的echo消息。但是当我们退出客户端,再次运行的时候,我们发消息就没响应了。这是因为我们服务端进入HandlerRequest之后死循环了。
继续修改HandlerRequest,当服务端read的返回值为0,说明客户端退出了。当read返回值小于0,说明读取失败,直接退出即可。
现在我们进程退出后读到0就会退出,但是还有个问题,就是文件描述符一直增加,这是因为我们服务端在读取到0,表明客户端退出,服务端退出HandlerRequest逻辑的时候并没有把文件关掉,所以我们在HandlerRequest函数最后添加close函数。
如果不关闭文件,那么fd的值就会一直增加,而fd属于有用的、有限的资源,如果不关闭就会导致fd泄漏问题。
另外如果客户端很多的话那fd不是会一直增加吗,如果fd只有32、64那不就不够用了吗?确实如此,Linux是支持对文件描述符个数进行扩展的,默认云服务器的fd数量是比较多的。可以使用ulimit -a查看:
如上图,open files就是fd的数量。
2、V2——EchoServer多进程版
上面的代码我们已经实现了单进程版,当服务端获取新连接,就会去执行HandlerRequest,这时候如果再来一个客户端就无法处理了,只能处理完当前客户端才能回到Start中再次获取新连接继续处理。也就是服务端当前只能处理一个客户端请求。因此我们需要将代码改成多进程版本,让服务端支持处理多个客户端。
原来是直接去执行HandlerRequest函数,现在我们创建子进程来执行HandlderRequest。由于创建子进程后,子进程拷贝父进程的文件描述符表,它们是各自有一份的,所以将子进程的listensockfd关闭,将父进程的sockfd关闭。但是这样还有个问题,就是父进程需要对子进程进行回收,否则子进程退出后就会僵尸,导致内存泄漏。而父进程如果等待子进程就会阻塞住,这样就跟单进程版没啥区别了。
下面有两种解决办法:
1、父进程直接使用signal函数将17号信号SIGCHLD主动设置为忽略。这样子进程退出后由操作系统自动回收。
2、父进程创建子进程,子进程中继续创建子进程,由孙子进程去执行HandlerRequest,子进程直接退出。这样孙子进程就会变成孤儿进程,孙子进程会被1号进程——操作系统领养,等将来孙子进程退出时由操作系统回收释放。然后父进程直接waitpid,由于子进程创建进程后直接退出,所以父进程waitpid不会阻塞,直接回收子进程,然后继续获取新连接。
我们使用第二种办法:
下面进行测试:
可以看到现在已经可以处理多个客户端了,并且每次获取新连接返回的文件描述符都是4。
可以看到有两个孙子进程,它们的父进程都是1号进程,将来客户端退出,这两个进程读到0退出就会由操作系统回收释放。
3、V3——EchoServer多线程版
创建进程还是一个比较重的工作,需要创建地址空间、页表等。所以接下来我们实现一个多线程版本。
创建线程执行ThreadEntry,而由于回调函数必须是返回值为void*,参数为void*的函数,因此不能直接执行HandlerRequest函数。现在又有很多问题:
1、ThreadEntry是类内函数,所以有一个隐含的this指针。因此我们需要将ThreadEntry设置static。
2、设置为static后线程可以执行ThreadEntry函数了,但是需要在ThreadEntry里面继续调用HandlerRequest函数,而调用HandlerRequest函数还是需要this指针,因此我们可以将this指针和sockfd封装在一个结构体里面传给ThreadEntry函数。
3、线程也需要等待,否则会有类似僵尸进程的问题。我们可以直接在ThreadEntry让线程自己分离。
如图多线程这里线程共享文件描述符表,因此主线程不敢随便close。可以看到sockfd会一直增加。
4、V4——EchoServer线程池版
在线程互斥与同步我们写过一个线程池,我们可以拿过来用。主线程将获取的新连接通过lambda或bind加入任务队列中。
定义一个task_t类型,然后加入到线程池的任务队列中。
HandlerRequest获取和发送数据我们使用的是read和write。接下来介绍两个接口:recv和send
在这里我们还可以使用recv读取,flags设置为0即可,阻塞读取。
还可以使用send发送,flag设置为0即可,阻塞发送。
// TcpServer.hpp
#pragma once#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"#define BACKLOG 8using namespace LogModule;
using namespace ThreadPoolModule;const static uint16_t gport = 8080;class TcpServer
{using task_t = std::function<void()>;struct ThreadData{int sockfd;TcpServer* self;};
public:TcpServer(uint16_t port = gport):_port(port),_isrunning(false){}void InitServer(){// 1.创建套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;// 2.填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); if (n < 0){LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;// 3.设置套接字为监听状态n = ::listen(_listensockfd, BACKLOG);if (n < 0){LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;}void HandlerRequest(int sockfd){LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];// 长任务while (true){int n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;std::string echo_string = "echo# ";echo_string += inbuffer;::send(sockfd, echo_string.c_str(), echo_string.size(), 0);}else if (n == 0){// 当读到0,表明客户端退出了。LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// n < 0说明读取失败break;}}::close(sockfd);}static void* ThreadEntry(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->self->HandlerRequest(td->sockfd);delete td; return nullptr;}void Start(){_isrunning = true;while (_isrunning){LOG(LogLevel::DEBUG) << "accept ing...";struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_listensockfd, CONV(&peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;} LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << "[" << addr.Addr() << "]";// version-0 单进程版// HandlerRequest(sockfd);// version-1 多进程版// pid_t id = fork();// if (id == 0)// {// ::close(_listensockfd);// if (fork() > 0) exit(0);// HandlerRequest(sockfd);// exit(0);// }// ::close(sockfd);// int rid = waitpid(id, nullptr, 0); // 不会阻塞.// if (rid < 0)// {// LOG(LogLevel::WARNING) << "waitpid error";// }// version-3 多线程版// pthread_t tid;// ThreadData* td = new ThreadData;// td->sockfd = sockfd;// td->self = this;// pthread_create(&tid, nullptr, ThreadEntry, td);// version-4 线程池版-比较适合处理短任务,或者是用户量少的情况// ThreadPool<task_t>::GetInstance()->Equeue(std::bind(&TcpServer::HandlerRequest, this, sockfd));ThreadPool<task_t>::GetInstance()->Equeue([this, sockfd](){this->HandlerRequest(sockfd);});}}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd;uint16_t _port;bool _isrunning;
};
下面进行测试:
但是我们今天这里的HandlerRequest是长任务,所以并不适合线程池。线程池比较适合短任务,用户量少的情况。
5、V5——多线程远程命令执行
我们可以让客户端输入命令,然后服务端接受数据做处理,将命令执行的结果返回给客户端。
添加一个handler_t类型,然后添加为TcpServer的成员变量,将来上层通过构造函数传入回调函数,在HandlerRequest中调用回调函数获取命令执行结构,然后将命令执行结果返回给客户端。
下面就需要实现CommonExec.hpp:
Execute就是将来上层要将传入回调函数执行的方法。首先需要fork创建子进程,可以让子进程重定向,将输出重定向到管道的写端,然后执行exec*程序替换,接着将结果写到管道里面,父进程再从管道读取结果。今天我们就不这么写了,介绍两个函数:
popen会创建管道,创建子进程进行程序替换执行命令,参数command就是命令字符串,type表示读写,我们设置为读就可以。返回值是FILE*,将来通过返回值可以读取命令执行的结果。然后使用pclose关闭FILE。
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <set>const int line_size = 1024;class Command
{
public:Command(){_white_list.insert("ls");_white_list.insert("pwd");_white_list.insert("ls -l");_white_list.insert("ll");_white_list.insert("ls -a -l");_white_list.insert("who");_white_list.insert("whoami");}bool SafeCheck(const std::string& cmdstr){auto iter = _white_list.find(cmdstr);return iter != _white_list.end();}std::string Execute(std::string cmdstr){// 1.pipe// 2.fork + dup2(pipe[1], 1) + exec*// 3.父进程读取if (!SafeCheck(cmdstr)) return cmdstr + " 不支持!";FILE* fp = popen(cmdstr.c_str(), "r");if (fp == nullptr){return "Failed";}std::string result;char buffer[line_size];while (true){char* p = ::fgets(buffer, sizeof(buffer), fp);if (p == nullptr) break;result += buffer;}pclose(fp);return result.empty() ? "Done" : result;}
private:std::set<std::string> _white_list;
};
使用set来保存运行执行的命令,Execute函数内部调用SafeCheck进行判断,如果不合法直接返回。
下面在TcpServer.cc传入回调函数:
6、验证TCP——Windows作为client访问Linux
下面这份代码在windows的vs2022下运行,windows作为客户端访问Linux,客户端发送消息,服务端返回echo# 消息。
#include <iostream>
#include <string>
#include <cstring>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996) // 去除使用inet_addr的警告
#pragma comment(lib, "ws2_32.lib") // 指定要链接的库std::string serverip = "47.117.157.14"; // 服务器IP
uint16_t serverport = 8080; // 服务器端口号int main()
{WSADATA wsd; // 定义winsock初始化信息结构体WSAStartup(MAKEWORD(2, 2), &wsd); // 初始化winsock库SOCKET sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd == INVALID_SOCKET){std::cout << "create socket error" << std::endl;WSACleanup(); // 清理并释放winsock资源return 1;}struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = ::htons(serverport);server.sin_addr.s_addr = ::inet_addr(serverip.c_str());int n = ::connect(sockfd, (const sockaddr*)&server, sizeof(server));if (n == SOCKET_ERROR){std::cout << "connect error" << std::endl;closesocket(sockfd);WSACleanup(); // 清理并释放winsock资源}char buffer[4096];while (true){std::cout << "Please Enter@ ";std::string line;std::getline(std::cin, line);n = ::send(sockfd, line.c_str(), line.size(), 0);if (n > 0){n = ::recv(sockfd, buffer, sizeof(buffer), 0);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}}closesocket(sockfd);WSACleanup(); // 清理并释放winsock资源return 0;
}
7、connect的断线重连
客户端会面临服务器崩溃的情况,我们可以试着写一个客户端重连的代码,模拟并理解一些客户端行为,比如游戏客户端断线重连。
采用状态机,实现一个简单的tcp client可以实现重连效果。
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>enum class Status
{NEW, // 新建状态CONNECTING, // 正在连接,仅方便查看conn状态CONNECTED, // 连接成功DISCONNECTED, // 连接失败或重连失败CLOSED // 连接失败,经过重连后还是失败。
};enum ExitCode
{USAGE_ERR = 1,SOCKET_ERR,
};const static int defaultsockfd = -1;
const static int defaultretryinterval = 1;
const static int defaultmaxretries = 5;class ClientConnection
{
public:ClientConnection(const std::string &serverip, uint16_t serverport): _sockfd(defaultsockfd), _serverip(serverip), _serverport(serverport), _status(Status::NEW), _retry_interval(defaultretryinterval), _max_retries(defaultmaxretries){}void Connect(){}void Reconnect(){}void Process(){}void Disconnect(){}Status GetStatus() { return _status; }~ClientConnection(){}private:int _sockfd;std::string _serverip; // 服务器IPuint16_t _serverport; // 服务器端口Status _status; // 当前连接状态int _retry_interval; // 重连时间间隔int _max_retries; // 最大重连次数
};class TcpClient
{
public:TcpClient(const std::string &serverip, uint16_t serverport): _connection(serverip, serverport){}void Execute(){while (true){switch (_connection.GetStatus()){case Status::NEW:_connection.Connect();break;case Status::CONNECTED:_connection.Process();break;case Status::DISCONNECTED:_connection.Reconnect();break;case Status::CLOSED:_connection.Disconnect();return;default:break;}}}~TcpClient(){}private:ClientConnection _connection;
};// ./client_tcp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;exit(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);TcpClient client(serverip, serverport);client.Execute();return 0;
}
如上,我们通过命令行参数将服务端ip和端口号传给进程,然后调用TcpServer构造函数传入,创建出TcpServer对象后就去执行Execute函数,该函数通过对ClientConnection对象中状态判断执行哪个函数。
如果当前状态为NEW,表示处于新建状态,就执行Connect函数建立连接。
如果当前状态为CONNECTED,表示已建立连接,就执行Process发送数据给服务端并接收服务端返回的数据。
如果当前状态为DISCONNECTED,表示建立连接失败,执行Reconnect重新连接服务端。
如果当前状态为CLOSED,表示经过重连后还是失败,所以直接关闭sockfd退出。
那么ClientConnection中就需要文件描述符sockfd,服务端IP和端口号,当前ClientConnection状态,重连时间间隔和最大重连次数。
实现Connect函数:
void Connect()
{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){std::cerr << "create socket error" << std::endl;exit(SOCKET_ERR);}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);inet_pton(AF_INET, _serverip.c_str(), &server.sin_addr.s_addr);int n = connect(_sockfd, (const sockaddr*)&server, sizeof(server));if (n < 0){Disconnect(); // 关闭sockfd_status = Status::DISCONNECTED; // 连接失败return;}// 连接成功_status = Status::CONNECTED;
}
创建套接字,然后进行连接,对连接返回值进行判断,如果小于说明连接失败。调用Disconnect关闭之前打开的sockfd。然后将状态设置为DISCONNECTED直接返回。那么在Execute函数中下一次循环就会去执行Reconnect进行重连。
实现Reconnect函数:
void Reconnect()
{_status = Status::CONNECTING;int count = 0;while (count < _max_retries){// _status = Status::CONNECTING;Connect();if (_status == Status::CONNECTED)return;++count;std::cout << "正在重连..., 重连次数: " << count << std::endl;sleep(_retry_interval);}_status = Status::CLOSED;std::cout << "重连失败,请检查你的网络..." << std:: endl;
}
将状态设置为CONNECTING表示正在连接,然后循环调用Connect,调用后对状态进行判断,如果为CONNECTED表示连接成功直接返回。如果最后达到最大连接次数还是失败,设置状态为CLOSED。
实现Process函数:
void Process()
{while (true){std::string line;std::cout << "Please Enter@ ";std::getline(std::cin, line);int n = send(_sockfd, line.c_str(), line.size(), 0);if (n > 0){char buffer[1024];int m = recv(_sockfd, buffer, sizeof(buffer)-1, 0);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else // 读取失败或断开连接{_status = Status::DISCONNECTED;break;}}else{std::cerr << "send error" << std::endl;_status = Status::CLOSED;// _status = Status::DISCONNECTED;break;}}
}
Process就是进行简单的IO操作,当recv读取数据m==0,说明服务端关闭连接或掉线了,我们进行重连。
最后Disconnect就是关闭sockfd:
void Disconnect()
{if (_sockfd > defaultsockfd){close(_sockfd);_sockfd = -1;}
}
完整代码如下:
// TcpClient.cc
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>enum class Status
{NEW, // 新建状态CONNECTING, // 正在连接,仅方便查看conn状态CONNECTED, // 连接成功DISCONNECTED, // 连接失败或重连失败CLOSED // 经过重连后还是失败。
};enum ExitCode
{USAGE_ERR = 1,SOCKET_ERR,
};const static int defaultsockfd = -1;
const static int defaultretryinterval = 1;
const static int defaultmaxretries = 5;class ClientConnection
{
public:ClientConnection(const std::string &serverip, uint16_t serverport): _sockfd(defaultsockfd), _serverip(serverip), _serverport(serverport), _status(Status::NEW), _retry_interval(defaultretryinterval), _max_retries(defaultmaxretries){}void Connect(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){std::cerr << "create socket error" << std::endl;exit(SOCKET_ERR);}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);inet_pton(AF_INET, _serverip.c_str(), &server.sin_addr);int n = connect(_sockfd, (const sockaddr*)&server, sizeof(server));if (n < 0){Disconnect(); // 关闭sockfd_status = Status::DISCONNECTED; // 连接失败return;}// 连接成功_status = Status::CONNECTED;}void Reconnect(){_status = Status::CONNECTING;int count = 0;while (count < _max_retries){// _status = Status::CONNECTING;Connect();if (_status == Status::CONNECTED)return;++count;std::cout << "正在重连..., 重连次数: " << count << std::endl;sleep(_retry_interval);}_status = Status::CLOSED;std::cout << "重连失败,请检查你的网络..." << std:: endl;}void Process(){while (true){std::string message = "hello server";int n = send(_sockfd, message.c_str(), message.size(), 0);if (n > 0){char buffer[1024];int m = recv(_sockfd, buffer, sizeof(buffer)-1, 0);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else // 读取失败或断开连接{_status = Status::DISCONNECTED;break;}}else{std::cerr << "send error" << std::endl;_status = Status::CLOSED;// _status = Status::DISCONNECTED;break;}sleep(1);}}void Disconnect(){if (_sockfd > defaultsockfd){close(_sockfd);_sockfd = -1;}}Status GetStatus() { return _status; }~ClientConnection(){}private:int _sockfd;std::string _serverip; // 服务器IPuint16_t _serverport; // 服务器端口Status _status; // 当前连接状态int _retry_interval; // 重连时间间隔int _max_retries; // 最大重连次数
};class TcpClient
{
public:TcpClient(const std::string &serverip, uint16_t serverport): _connection(serverip, serverport){}void Execute(){while (true){switch (_connection.GetStatus()){case Status::NEW:_connection.Connect();break;case Status::CONNECTED:_connection.Process();break;case Status::DISCONNECTED:_connection.Reconnect();break;case Status::CLOSED:_connection.Disconnect();return;default:break;}}}~TcpClient(){}private:ClientConnection _connection;
};// ./client_tcp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;exit(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);TcpClient client(serverip, serverport);client.Execute();return 0;
}
下面进行测试: