当前位置: 首页 > article >正文

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攻击&#xff0c;这类攻击会通过大量受控制的僵尸网络向目标服务器发送请求&#xff0c;以此来消耗服务器中的资源&#xff0c;致使用户无法正常访问&#xff0c;当网页受到分布式拒绝服务攻击时都有哪些应对方法呢&#xff1f; 建立全…...

文件映射mmap与管道文件

在用户态申请内存&#xff0c;内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说&#xff0c;把磁盘里的数据与内存的用户态建立一一映射关系&#xff0c;让读写内存等价于读写磁盘&#xff0c;支持随机访问。 管道文件&#xff1a;进程间通信机…...

4.4刷题记录(哈希表)

1.242. 有效的字母异位词 - 力扣&#xff08;LeetCode&#xff09; 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地址 本期本来是很有难度的&#xff0c;不过 大家做完 分割回文串 之后&#xff0c;本题就容易很多了 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;回溯算法如何分割字符串并判断是合法IP&#xff1f;| LeetCode&#xff1a;93.复原IP地址_哔哩哔…...

批量改CAD图层颜色——CAD c#二次开发

一个文件夹下大量图纸&#xff08;几百甚至几千个文件&#xff09;需要改图层颜色时&#xff0c;可采用插件实现&#xff0c;效果如下&#xff1a; 转换前&#xff1a; 转换后&#xff1a; 使用方式如下&#xff1a;netload加载此dll插件&#xff0c;输入xx运行。 附部分代码如…...

【内网安全】DHCP 饿死攻击和防护

正常情况&#xff1a;PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包&#xff1a;大量的DHCP Discover包 此时模…...

【愚公系列】《高效使用DeepSeek》055-可靠性评估与提升

🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...

AI时代编程教育启示录:为什么基础原理依然不可或缺?

李升伟 编译 在生成式AI重塑编程教育的今天&#xff0c;我作为拥有十年开发者关系团队管理经验、编程训练营教学经历的专业软件工程师&#xff0c;想与大家探讨这个新时代的编程教育之道。 ‌平衡之道&#xff1a;基础原理与AI工具的博弈‌ 当GitHub Copilot、Amazon Q Deve…...

10种电阻综合对比——《器件手册--电阻》

二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件&#xff0c;通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻&#xff0c;适合高密度电路板&#xff1b;精度高、稳定性好&#xff0c;便于自动化生产 广泛应用于…...

剑指Offer(数据结构与算法面试题精讲)C++版——day6

剑指Offer&#xff08;数据结构与算法面试题精讲&#xff09;C版——day6 题目一&#xff1a;不含重复字符的最长子字符串题目二&#xff1a;包含所有字符的最短字符串题目三&#xff1a;有效的回文 题目一&#xff1a;不含重复字符的最长子字符串 这里还是可以使用前面&#x…...

freertos韦东山---事件组以及实验

事件组的原理是什么&#xff0c;有哪些优点&#xff0c;为啥要创造出这个概念 在实时操作系统&#xff08;如 FreeRTOS&#xff09;中&#xff0c;事件组是一种用于任务间同步和通信的机制&#xff0c;它的原理、优点及存在意义如下&#xff1a; 事件组原理 数据结构&#xf…...

架构师面试(二十六):系统拆分

问题 今天我们聊电商系统实际业务场景的问题&#xff0c;考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速&#xff0c;DAU在 10W&#xff1b;整个电商系统按水平分层架构进行设计&#xff0c;包括【入口网关…...

Spring 中的事务

&#x1f9fe; 一、什么是事务&#xff1f; &#x1f9e0; 通俗理解&#xff1a; 事务 一组操作&#xff0c;要么全部成功&#xff0c;要么全部失败&#xff0c;不能只做一半。 比如你转账&#xff1a; A 账户扣钱B 账户加钱 如果 A 扣了钱但 B 没收到&#xff0c;那就出问…...

Java中的同步和异步

一、前言 在Java中&#xff0c;同步&#xff08;Synchronous&#xff09;和异步&#xff08;Asynchronous&#xff09;是两种不同的任务处理模式。核心区别在任务执行的顺序控制和线程阻塞行为。 二、同步&#xff08;Synchronous&#xff09; 定义&#xff1a;任务按顺序执行…...

vue2 vue3 响应式差异

vue2 响应式原理看这 链接: link 总结&#xff1a; object.defineproperty()是对属性的劫持&#xff0c;对属性劫持有两大缺陷 1. 需要遍历对象的所有属性&#xff0c;深层属性需递归&#xff0c;存在效率问题 2. 后添加的属性&#xff0c;无法获得响应式&#xff0c;因为劫持…...

唯一ID生成器设计方案

《亿级流量系统架构设计与实战》总结 1. 唯一ID的核心需求 • 全局唯一性&#xff1a;分布式系统中所有节点生成的ID不可重复。 • 趋势递增性&#xff08;可选&#xff09;&#xff1a;ID按时间或序列递增&#xff0c;优化数据库写入性能。 • 高可用性&#xff1a;服务需72…...

OpenCV 图形API(16)将极坐标(magnitude 和 angle)转换为笛卡尔坐标(x 和 y)函数polarToCart()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 描述 计算二维向量的 x 和 y 坐标。 polarToCart 函数根据 magnitude 和 angle 的对应元素表示的每个二维向量&#xff0c;计算其笛卡尔坐标&#xff1a;…...

在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus

一、安装环境信息说明 硬件资源&#xff08;GB 和 GiB 的主要区别在于它们的换算基数不同&#xff0c;GB 使用十进制&#xff0c;GiB 使用二进制&#xff0c;导致相同数值下 GiB 表示的容量略大于 GB&#xff1b;换算关系&#xff1a;1 GiB ≈ 1.07374 GB &#xff1b;1 GB ≈ …...

安装和配置Docker

其他版本的安装方式可直接参考官方网站&#xff0c;推荐通过官方网站提供的方式安装Dockers&#xff0c;下面只是个演示的示例&#xff0c;仅供参考 Install | Docker Docs 安装 Docker 的前置准备 1.虚拟机配置&#xff1a; 推荐配置 内存&#xff1a;4GB&#xff08;最低…...

Ansible YAML 基础语法与关键词 的详细指南

以下是 Ansible YAML 基础语法与关键词 的详细指南&#xff0c;帮助你快速掌握 Playbook 编写规范和核心概念&#xff1a; 目录 一、Ansible Playbook 基础结构1. YAML 文件基础 二、核心关键词1. Play 定义2. Task 定义3. Handler 定义4. 变量&#xff08;Variables&#xff0…...

NO.64十六届蓝桥杯备战|基础算法-简单贪心|货仓选址|最大子段和|纪念品分组|排座椅|矩阵消除(C++)

贪⼼算法是两极分化很严重的算法。简单的问题会让你觉得理所应当&#xff0c;难⼀点的问题会让你怀疑⼈⽣ 什么是贪⼼算法&#xff1f; 贪⼼算法&#xff0c;或者说是贪⼼策略&#xff1a;企图⽤局部最优找出全局最优。 把解决问题的过程分成若⼲步&#xff1b;解决每⼀步时…...

瑞萨RA4M2使用心得-KEIL5的第一次编译

目录 前言 环境&#xff1a; 开发板&#xff1a;RA-Eco-RA4M2-100PIN-V1.0 IDE&#xff1a;keil5.35 一、软件的下载 编辑瑞萨的芯片&#xff0c;除了keil5 外还需要一个软件&#xff1a;RASC 路径&#xff1a;Releases renesas/fsp (github.com) 向下找到&#xff1a; …...

java根据集合中对象的属性值大小生成排名

1&#xff1a;根据对象属性降序排列 public static <T extends Comparable<? super T>> LinkedHashMap<T, Integer> calculateRanking(List<ProductPerformanceInfoVO> dataList, Function<ProductPerformanceInfoVO, T> keyExtractor) {Linked…...

数据分析-Excel-学习笔记

Day1 复现报表聚合函数&#xff1a;日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格&#xff0c;首先要看这个表格个构成&#xff08;包含了哪些数据&#xff09;&#xff0c;几行几列&#xff0c;每一列的名称…...

整车CAN网络和CANoe

车载网络中主要包含有Can网络,Lin网络,FlexRay,Most,以太网。 500kbps:500波特率,表示的数据传输的速度。表示的是最大的网速传输速度。也就是每秒 500kb BodyCan车身Can InfoCan娱乐信息Can 车身CAN主要连接的是ESB电动安全带 ADB自适应远光灯等 PTCan动力Can 底盘Can...

ChatGPT 的新图像生成器非常擅长伪造收据

本月&#xff0c;ChatGPT 推出了一种新的图像生成器&#xff0c;作为其 4o 模型的一部分&#xff0c;该模型在生成图像内的文本方面做得更好。 人们已经在利用它来生成假的餐厅收据&#xff0c;这可能会为欺诈者使用的已经很广泛的 AI 深度伪造工具包添加另一种工具。 多产的…...

JS页面尺寸事件

元素位置 在这里插入图片描述 父元素带有定位时输出相对于父亲元素的距离值...

SpringBoot的日志框架

目录 默认日志框架 日志配置 更换日志框架 排除默认Logback 引入目标日志框架 添加配置文件 logback.xml SpringBoot的核心设计宗旨是约定大于配置,很多框架功能都给你默认加载和配置完成供你使用,但这就要求使用者对框架有一定的理解和改造能力,比如这个日志框架,是其…...

网络协议之基础介绍

写在前面 本文看下网络协议相关基础内容。 1&#xff1a;为什么要有网络协议 为了实现世界各地的不同主机的互联互通。 2&#xff1a;协议的三要素 协议存在的目的就是立规矩&#xff0c;无规矩不成方圆嘛&#xff01;但是这个规矩也不是想怎么立就怎么立的&#xff0c;也…...