Socket编程TCP
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;
}
下面进行测试:

相关文章:
Socket编程TCP
Socket编程TCP 1、V1——EchoServer单进程版2、V2——EchoServer多进程版3、V3——EchoServer多线程版4、V4——EchoServer线程池版5、V5——多线程远程命令执行6、验证TCP——Windows作为client访问Linux7、connect的断线重连 1、V1——EchoServer单进程版 在TcpServer.hpp中实…...
当网页受到DDOS网络攻击有哪些应对方法?
分布式拒绝服务攻击也是人们较为熟悉的DDOS攻击,这类攻击会通过大量受控制的僵尸网络向目标服务器发送请求,以此来消耗服务器中的资源,致使用户无法正常访问,当网页受到分布式拒绝服务攻击时都有哪些应对方法呢? 建立全…...
文件映射mmap与管道文件
在用户态申请内存,内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说,把磁盘里的数据与内存的用户态建立一一映射关系,让读写内存等价于读写磁盘,支持随机访问。 管道文件:进程间通信机…...
4.4刷题记录(哈希表)
1.242. 有效的字母异位词 - 力扣(LeetCode) class Solution { public:bool isAnagram(string s, string t) {unordered_map<char,int>cnt_s,cnt_t;for(int i0;i<s.size();i){cnt_s[s[i]];}for(int i0;i<t.size();i){cnt_t[t[i]];}if(cnt_sc…...
代码随想录回溯算法03
93.复原IP地址 本期本来是很有难度的,不过 大家做完 分割回文串 之后,本题就容易很多了 题目链接/文章讲解:代码随想录 视频讲解:回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址_哔哩哔…...
批量改CAD图层颜色——CAD c#二次开发
一个文件夹下大量图纸(几百甚至几千个文件)需要改图层颜色时,可采用插件实现,效果如下: 转换前: 转换后: 使用方式如下:netload加载此dll插件,输入xx运行。 附部分代码如…...
【内网安全】DHCP 饿死攻击和防护
正常情况:PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包:大量的DHCP Discover包 此时模…...
【愚公系列】《高效使用DeepSeek》055-可靠性评估与提升
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...
AI时代编程教育启示录:为什么基础原理依然不可或缺?
李升伟 编译 在生成式AI重塑编程教育的今天,我作为拥有十年开发者关系团队管理经验、编程训练营教学经历的专业软件工程师,想与大家探讨这个新时代的编程教育之道。 平衡之道:基础原理与AI工具的博弈 当GitHub Copilot、Amazon Q Deve…...
10种电阻综合对比——《器件手册--电阻》
二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件,通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻,适合高密度电路板;精度高、稳定性好,便于自动化生产 广泛应用于…...
剑指Offer(数据结构与算法面试题精讲)C++版——day6
剑指Offer(数据结构与算法面试题精讲)C版——day6 题目一:不含重复字符的最长子字符串题目二:包含所有字符的最短字符串题目三:有效的回文 题目一:不含重复字符的最长子字符串 这里还是可以使用前面&#x…...
freertos韦东山---事件组以及实验
事件组的原理是什么,有哪些优点,为啥要创造出这个概念 在实时操作系统(如 FreeRTOS)中,事件组是一种用于任务间同步和通信的机制,它的原理、优点及存在意义如下: 事件组原理 数据结构…...
架构师面试(二十六):系统拆分
问题 今天我们聊电商系统实际业务场景的问题,考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速,DAU在 10W;整个电商系统按水平分层架构进行设计,包括【入口网关…...
Spring 中的事务
🧾 一、什么是事务? 🧠 通俗理解: 事务 一组操作,要么全部成功,要么全部失败,不能只做一半。 比如你转账: A 账户扣钱B 账户加钱 如果 A 扣了钱但 B 没收到,那就出问…...
Java中的同步和异步
一、前言 在Java中,同步(Synchronous)和异步(Asynchronous)是两种不同的任务处理模式。核心区别在任务执行的顺序控制和线程阻塞行为。 二、同步(Synchronous) 定义:任务按顺序执行…...
vue2 vue3 响应式差异
vue2 响应式原理看这 链接: link 总结: object.defineproperty()是对属性的劫持,对属性劫持有两大缺陷 1. 需要遍历对象的所有属性,深层属性需递归,存在效率问题 2. 后添加的属性,无法获得响应式,因为劫持…...
唯一ID生成器设计方案
《亿级流量系统架构设计与实战》总结 1. 唯一ID的核心需求 • 全局唯一性:分布式系统中所有节点生成的ID不可重复。 • 趋势递增性(可选):ID按时间或序列递增,优化数据库写入性能。 • 高可用性:服务需72…...
OpenCV 图形API(16)将极坐标(magnitude 和 angle)转换为笛卡尔坐标(x 和 y)函数polarToCart()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 描述 计算二维向量的 x 和 y 坐标。 polarToCart 函数根据 magnitude 和 angle 的对应元素表示的每个二维向量,计算其笛卡尔坐标:…...
在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus
一、安装环境信息说明 硬件资源(GB 和 GiB 的主要区别在于它们的换算基数不同,GB 使用十进制,GiB 使用二进制,导致相同数值下 GiB 表示的容量略大于 GB;换算关系:1 GiB ≈ 1.07374 GB ;1 GB ≈ …...
安装和配置Docker
其他版本的安装方式可直接参考官方网站,推荐通过官方网站提供的方式安装Dockers,下面只是个演示的示例,仅供参考 Install | Docker Docs 安装 Docker 的前置准备 1.虚拟机配置: 推荐配置 内存:4GB(最低…...
Ansible YAML 基础语法与关键词 的详细指南
以下是 Ansible YAML 基础语法与关键词 的详细指南,帮助你快速掌握 Playbook 编写规范和核心概念: 目录 一、Ansible Playbook 基础结构1. YAML 文件基础 二、核心关键词1. Play 定义2. Task 定义3. Handler 定义4. 变量(Variables࿰…...
NO.64十六届蓝桥杯备战|基础算法-简单贪心|货仓选址|最大子段和|纪念品分组|排座椅|矩阵消除(C++)
贪⼼算法是两极分化很严重的算法。简单的问题会让你觉得理所应当,难⼀点的问题会让你怀疑⼈⽣ 什么是贪⼼算法? 贪⼼算法,或者说是贪⼼策略:企图⽤局部最优找出全局最优。 把解决问题的过程分成若⼲步;解决每⼀步时…...
瑞萨RA4M2使用心得-KEIL5的第一次编译
目录 前言 环境: 开发板:RA-Eco-RA4M2-100PIN-V1.0 IDE:keil5.35 一、软件的下载 编辑瑞萨的芯片,除了keil5 外还需要一个软件:RASC 路径:Releases renesas/fsp (github.com) 向下找到: …...
java根据集合中对象的属性值大小生成排名
1:根据对象属性降序排列 public static <T extends Comparable<? super T>> LinkedHashMap<T, Integer> calculateRanking(List<ProductPerformanceInfoVO> dataList, Function<ProductPerformanceInfoVO, T> keyExtractor) {Linked…...
数据分析-Excel-学习笔记
Day1 复现报表聚合函数:日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格,首先要看这个表格个构成(包含了哪些数据),几行几列,每一列的名称…...
整车CAN网络和CANoe
车载网络中主要包含有Can网络,Lin网络,FlexRay,Most,以太网。 500kbps:500波特率,表示的数据传输的速度。表示的是最大的网速传输速度。也就是每秒 500kb BodyCan车身Can InfoCan娱乐信息Can 车身CAN主要连接的是ESB电动安全带 ADB自适应远光灯等 PTCan动力Can 底盘Can...
ChatGPT 的新图像生成器非常擅长伪造收据
本月,ChatGPT 推出了一种新的图像生成器,作为其 4o 模型的一部分,该模型在生成图像内的文本方面做得更好。 人们已经在利用它来生成假的餐厅收据,这可能会为欺诈者使用的已经很广泛的 AI 深度伪造工具包添加另一种工具。 多产的…...
JS页面尺寸事件
元素位置 在这里插入图片描述 父元素带有定位时输出相对于父亲元素的距离值...
SpringBoot的日志框架
目录 默认日志框架 日志配置 更换日志框架 排除默认Logback 引入目标日志框架 添加配置文件 logback.xml SpringBoot的核心设计宗旨是约定大于配置,很多框架功能都给你默认加载和配置完成供你使用,但这就要求使用者对框架有一定的理解和改造能力,比如这个日志框架,是其…...
网络协议之基础介绍
写在前面 本文看下网络协议相关基础内容。 1:为什么要有网络协议 为了实现世界各地的不同主机的互联互通。 2:协议的三要素 协议存在的目的就是立规矩,无规矩不成方圆嘛!但是这个规矩也不是想怎么立就怎么立的,也…...
