目录
[1. EchoServer 将TCP的接口串完](#1. EchoServer 将TCP的接口串完)
[2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连](#2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连)
1. EchoServer 将TCP的接口串完
创建套接字,必须得有一个创建套接字的接口:
socket()

不管是TCP还是UDP底层都需要网络功能,必定使用网络协议:

有些用的是PF_INET,和AF_INET 是等价的:

其实表示的就是IP协议族:

- type:选用套接字类型,基于流式套接

其实底层用IP,上层用流式套接其实就是TCP套接字。
- protocol设置为0。
- 返回值:成功得到一个文件描述符,失败就是-1。

TCP是面向连接的,服务端必须将套接字,设置为listen状态,TCP要随时准备被客户端连接
listen()

- int backlog:指的是底层将来全连接队列的数值-1,表示的是底层新连接到来时的队列长度
- 而全连接队列的长度会受到 listen 第二个参数的影响。
- 全连接队列满了的时候, 就⽆法继续让当前连接的状态进入established 状态了。
- 这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1。
如果将 backlog 这个参数设置为5,那么底层的全连接数为 6。 backlog 的值就为底层的全连接数-1。backlog 的值一般都是8、16、32。
- 返回值:成功为0,失败为-1

bash
# Makefile
.PHONY:all
all: tcp_client tcp_server
tcp_client:TcpClient.cc
g++ -o $@ $^ -std=c++17
tcp_server:TcpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f tcp_client tcp_server
cpp
//TcpEchoServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP_
#define __TCP_ECHO_SERVER_HPP_
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Logger.hpp"
static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
class TcpEchoServer
{
public:
TcpEchoServer(uint16_t port = gport)
: _sockfd(gdefaultfd), _port(port)
{
}
void Init()
{
// 1. 创建套接字文件描述符 create socket fd
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCKET_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _sockfd; // 3 套接字也是文件
// 2. bind socket fd
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(SOCKET_BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success: " << _sockfd;
// 3. set socket listen 设置套接字为监听状态
if (listen(_sockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCKET_LISTEN_ERR);
}
LOG(LogLevel::INFO) << "Listen socket success: " << _sockfd;
}
void Start()
{
while(true)
{
sleep(1);
}
}
~TcpEchoServer()
{
}
private:
int _sockfd; // 暂时
uint16_t _port;
};
#endif
cpp
//Comm.hpp
#ifndef __COMM_HPP_
#define __COMM_HPP_
#include <iostream>
enum
{
OK,
SOCKET_CREATE_ERR,
SOCKET_BIND_ERR,
SOCKET_LISTEN_ERR
};
#endif
cpp
//TcpServer.cc
#include "TcpEchoServer.hpp"
#include <memory>
int main()
{
EnableConsoleLogStrategy();
std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>();
tsvr->Init();
tsvr->Start();
return 0;
}
cpp
//TcpClient.cc
#include <iostream>
int main()
{
}
运行结果:

其实到这里就已经建立了一个TCP服务器了,是卡在了start类中的死循环中。


accept():获取新的连接。没有新连接,accept默认是阻塞的;如果有新连接,accept返回,获取新连接

- int sockfd:就是刚刚创建、绑定、监听的套接字
- struct sockaddr *addr:输出型参数,得到对方的套接字
- socklen_t *addrlen):输入输出型参数,输入表示的是:struct sockaddr in 结构对应的长度,返回的是实际上的套接字结构体的大小
- 以上的后面的两个参数和UDP中的recvfrom的后面两个参数是一样的,代表的是客户端是谁的问题。TCP是先将套接字的信息发过来。
- 返回值:成功返回一个文件描述符;失败,返回 -1。

tcp 是会产生更多的fd -- 是的!!返回的一个文件描述符代表的就是一个客户端。
理解 _sockfd vs accept return fd:
例子:
旅游景点,餐厅外基本上都有一个揽客的人,揽到客人之后让客人进入餐厅并叫一个服务员出来服务,服务员在服务客人的时候,刚刚那个揽客的人就有继续出去揽客去了,揽客的那个人只负责获取客源,就是我们刚刚创建的套接字,如今改名为:_listensockfd,而餐厅就是服务器,一桌一桌的客人就是客户端。监听套接字每获取一个新连接,就冲着OS说:我已经获得了一个新连接了,用系统调用accept赶紧获得一个服务员来提供服务,服务员代表的就是accept返回的文件描述符。

所以:

在TCP中,如果你要读写数据,跟揽客的那个sockfd没有关系,而是跟accept的返回值来进行IO读写。
客户端先写一部分,客户端和服务端的面向连接的这个特性先体现出来,客户端不需要显示bind,也不需要 listen 和 accept 获取新连接,但是需要客户端向目标服务器发起连接请求!!!客户端需要做的工作:创建好套接字,直接通过该套接字,向目标服务器发起建立连接的请求。所以说 tcp是面向连接的!!!
connect()

虽然说客户端不需要显示的绑定,但是确实是要进行绑定的!!!那么又在何时绑定的呢???
connect的返回值:连接成功,返回0。连接失败,返回-1。


说明,在发起 connet 的时候,底层是要做 bind 绑定的,当成功调用connect函数的时候,就会绑定本地的套接字,然后向目标主机服务器发起连接请求。所以在connect当中是要传 sockfd 这个套接字,显然,是要将文件信息和网络信息进行绑定。connect后面的两个参数,有点类似 sendto。

sendto 再发消息的时候,后两个参数表示的是要发给谁呀!表示的就是目标主机套接字信息和长度。
cpp
//InetAddr.hpp
#pragma once
// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端 -> 先描述再组织
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr *)&_addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
_addr.sin_port = htons(_port);
}
public:
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
Net2Host();
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr *Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip + "-" + std::to_string(_port);
}
bool operator==(const InetAddr &addr)
{
return (_ip == addr._ip && _port == addr._port); // 同时启动多个客户端
// return (_ip == addr._ip); // 只比较IP的话,好处就是只允许客户端启动一次
}
~InetAddr() {}
private:
struct sockaddr_in _addr; // 网络风格地址
// 主机风格地址
std::string _ip;
uint16_t _port;
};
cpp
//TcpClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "InetAddr.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}
// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "create client sockfd error" << std::endl;
exit(SOCKET_CREATE_ERR);
}
// tcp客户端,要不要显示的bind???-不要 要不要"bind"???-要
// 客户端自己的socket地址,让本地OS自主随机选择,尤其是端口号
// 客户端不需要listen 和 accpet获取新连接
// 客户端需要向目标服务器发起连接请求!!!
InetAddr server(serverport,serverip);
if(connect(sockfd,server.Addr(),server.Length()) != 0)
{
std::cerr << "connect server sockfd error" << std::endl;
exit(SOCKET_CONNECT_ERR);
// 这里不一定非得退出,失败后,将创建套接字和建立连接两段代码放在一个while循环中,建立连接失败让它continue,continue之前让它休眠上12秒钟,再次continue
// 释放老套接字,让他再重新创建新套接字 ,重新在建立再发起连接请求,再失败,在循环,尝试若干次之后,再让客户端退出,此时,就编写了一个客户端断线重连机制
}
std::cout << "connect " << server.ToString() << " success" << std::endl;
return 0;
}
cpp
//TcpEchoServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP_
#define __TCP_ECHO_SERVER_HPP_
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"
static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
class TcpEchoServer
{
public:
TcpEchoServer(uint16_t port = gport)
: _listensockfd(gdefaultfd), _port(port)
{
}
void Init()
{
// 1. 创建套接字文件描述符 create socket fd
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCKET_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd; // 3 套接字也是文件
// 2. bind socket fd
InetAddr local(_port);
if (bind(_listensockfd, local.Addr(), local.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(SOCKET_BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
// 3. set socket listen 设置套接字为监听状态
// 一个 tcp server ,一旦设置为 listen 状态之后,启动之后,服务器已经算是运行了
if (listen(_listensockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCKET_LISTEN_ERR);
}
LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;
}
void Start()
{
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 给客户提供IO服务,不能用 _listensockfd 来操作
int sockfd = accept(_listensockfd,(struct sockaddr*)&peer,&len);
if(sockfd<0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : "<< sockfd
<< " client addr: " << clientaddr.ToString();
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd; // 监听socket
uint16_t _port;
};
#endif
运行结果:




客户端只获取新连接,并没有关闭,所以获得的sockfd是4、5...
服务器端获取了连接,是要进行处理对应的文件描述符进行IO通信。
TCP是面向字节流的,读数据的时候就可以用 read() 系统调用。

返回值:返回的是-1表示的是读取出错了,如果返回值是等于0表示的是读到了文件的结尾,如果在网络中返回值为0,代表的是客户端关闭,

2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连
cpp
//TcpEchoServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__
#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
// TCP是面向连接,处理对应的连接时就会出现长连接和短连接的情况
// 长任务-> sockfd -> 长连接,不适合拿线程池来处理,长连接适合多路转接的技术,打游戏就是长连接的场景,客户端和服务端要进行持续的通信
// 聊天的场景是一种短连接的场景,前提是用的是TCP的协议
// 短任务-> 短连接
void HandlerIO(int sockfd, InetAddr client)
{
char buffer[1024];
while (true)
{
buffer[0] = 0;
// 约定:你给我发过来的是命令字符串!ls -a -l touch XX
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::string echo_string = "server echo# ";
echo_string += buffer;
LOG(LogLevel::DEBUG) << client.ToString() << "say: " << buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client "
<< client.ToString() << " quit, me too, close fd: " << sockfd;
break;
}
else
{
LOG(LogLevel::WARNING) << "read client "
<< client.ToString() << " error, sockfd : " << sockfd;
break;
}
}
close(sockfd); // 一定要关闭
}
public:
TcpEchoServer(uint16_t port = gport)
: _listensockfd(gdefaultfd), _port(port)
{
}
void Init()
{
// 1. create socket fd
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCKET_CREATE_ERR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd; // 3
// 2. bind socket fd
InetAddr local(_port);
if (bind(_listensockfd, local.Addr(), local.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(SOCKET_BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
// 3. set socket listen
// 一个tcp server,listen,启动之后,服务器已经算是运行了
if (listen(_listensockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCKET_LISTEN_ERR);
}
LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;
}
class ThreadData
{
public:
ThreadData(int sockfd, TcpEchoServer *self, const InetAddr &addr)
: _sockfd(sockfd), _self(self), _addr(addr)
{}
public:
int _sockfd;
TcpEchoServer *_self;
InetAddr _addr;
};
static void *Routine(void *args)
{
ThreadData *td = static_cast<ThreadData*>(args);
pthread_detach(pthread_self());
td->_self->HandlerIO(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
void Start()
{
// signal(SIGCHLD, SIG_IGN); // 最佳实践
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : " << sockfd
<< " client addr: " << clientaddr.ToString();
// 多进程,多线程
// 1. 效率问题,创建进程线程
// 2. 执行流个数没有上限
// 进程池4:
ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, clientaddr](){
this->HandlerIO(sockfd, clientaddr);
});
//version3 多线程做法
// 多线程是如何看待主线程曾经打开的文件描述符表!共享
// 主线程和新线程需要关闭历史fd吗?不需要!
// pthread_t tid;
// ThreadData *td = new ThreadData(sockfd, this, clientaddr);
// pthread_create(&tid, nullptr, Routine, (void*)td);
// handler sockfd
// version1 -- 单进程的!
// HandlerIO(sockfd, clientaddr);
// version2 --- 多进程
// 创建子进程,子进程是如何看待父进程的fd的?
// socketfd可以被子进程继承!
// pid_t id = fork();
// if (id < 0)
// {
// LOG(LogLevel::FATAL) << "资源不足,创建子进程失败";
// exit(FORK_ERR);
// }
// else if (id == 0)
// {
// // child -> sockfd -> 也能看到_listensockfd
// close(_listensockfd); //只是改了引用计数
// if(fork()>0)
// exit(OK);
// // 执行任务的是:孙子进程
// HandlerIO(sockfd, clientaddr); // while(true)
// exit(OK);
// }
// else
// {
// close(sockfd); // 1. 关闭无用fd 2. 规避fd泄漏!
// // father -> _listensockfd -> sockfd
// pid_t rid = waitpid(id, nullptr, 0);
// (void)rid;
// }
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd; // 监听socket
uint16_t _port;
};
#endif


cpp
//Main.cc
#include "CommandServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localport" << std::endl;
}
std::string CommandExec(const std::string &commandstr)
{
return "hello";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t serverport = std::stoi(argv[1]);
EnableConsoleLogStrategy();
std::unique_ptr<CommandServer> tsvr = std::make_unique<CommandServer>(CommandExec,serverport);
tsvr->Init();
tsvr->Start();
return 0;
}

证明网络和客户端之间是打通了的,接着要做的就是执行命令。
popen()函数:将对应的命令传递进来,命令就是一个完整的字符串,直接传递进来,popen就会在底层将命令执行完,如果命令有交互,有输入or输出数据,以特定的文件类型打开,打开成功之后,就会返回一个文件对象。简单来说,popen会将对应的命令执行完,把结果通过类似于文件操作的方式,通过FILE*将命令执行完的结果拿到,拿到之后返回。popen底层做的工作就是创建管道,然后再创建子进程,然后再让子进程程序替换,管道也是文件,将管道文件描述符包装成文件指针,进而返回,上层再通过读文件指针,把文件的内容给读出来了。之后再通过pclose对文件队形进行关闭。

返回值:

cpp
//Command.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
class Command
{
private:
bool IsSafe(const std::string &cmd)
{
for(auto &c : _command_Whitelist)
{
if(cmd == c)
{
return true;
}
}
return false;
}
public:
Command()
{
_command_Whitelist.push_back("ls -a -l");
_command_Whitelist.push_back("pwd");
_command_Whitelist.push_back("ll");
_command_Whitelist.push_back("cat test.txt");
_command_Whitelist.push_back("touch touch.txt");
_command_Whitelist.push_back("tree");
_command_Whitelist.push_back("whoami");
_command_Whitelist.push_back("who");
}
std::string Exec(const std::string &cmd)
{
if(!IsSafe(cmd))
{
return "坏人";
}
std::string result;
FILE *fp = popen(cmd.c_str(),"r");
if (fp == NULL)
{
result = cmd + " exec error";
}
else
{
char buffer[1024];
while(fgets(buffer,sizeof(buffer),fp) !=nullptr )
{
result += buffer;
}
pclose(fp);
}
return result;
}
~Command(){}
private:
std::vector<std::string> _command_Whitelist;
};


inet_ntoa()接口:将一个4字节的网络地址转换为一个字符串,所对应的返回值是一个char*类型的,实际上返回的是一种字符串风格的IP地址:192.168.1.100,指针返回,字符串在哪里保存?


所以,就不太推荐inet_ntoa()此接口。
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP
地址。但是我们通常用点分⼗进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr(字符串转4字节的)的函数:
cpp
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);
in_addr转字符串(4字节转字符串)的函数:
cpp
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是
void *addrptr。
关于inet_ntoa
inet_ntoa这个函数返回了⼀个char*, 很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的结果. 那么是否需要调用者⼿动释放呢?

man⼿册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们⼿动进⾏释放。
但是如果我们单进程或者是多线程的去调用以下的代码的时候:

实际的运行结果:

因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区, 这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果。
要解决上面的问题就是:每一个用户调函数的时候传一个自己的缓冲区,不要用公共的缓冲区。
最佳实践:
字符串转in_addr(字符串转4字节的)的函数:int inet_pton(int af, const char *src, void *dst);
- int af:inet
- const char *src:要转的字符串,输入型参数,带有const
- void *dst:输出型参数
主机转网络的时候需要将字符串转为4字节
cpp
//InetAddr.hpp
#pragma once
// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端 -> 先描述再组织
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr *)&_addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET,&(_addr.sin_addr.s_addr),ipbuffer,sizeof(ipbuffer));
_ip = ipbuffer;
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr.s_addr));
}
public:
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
Net2Host();
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr *Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip + "-" + std::to_string(_port);
}
bool operator==(const InetAddr &addr)
{
return (_ip == addr._ip && _port == addr._port); // 同时启动多个客户端
// return (_ip == addr._ip); // 只比较IP的话,好处就是只允许客户端启动一次
}
~InetAddr() {}
private:
struct sockaddr_in _addr; // 网络风格地址
// 主机风格地址
std::string _ip;
uint16_t _port;
};
运行结果:


但是这里是有bug的:

UDP在读写的是不存在问题的,但是TCP是存在问题的,因为TCP是面向字节流的。