Socket编程-TCP
1. 相关接口和命令
1.1 设置套接字为监听状态-listen
c
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能 :listen() 是一个系统调用函数,用于将一个主动连接的套接字(socket)转换为被动监听状态,以便接受来自客户端的连接请求。它通常在 bind() 之后、accept() 之前调用。
参数说明:
- sockfd :通过
socket()创建并经过bind()绑定地址和端口的套接字文件描述符。 - backlog :指定等待连接队列的最大长度。当有新的连接请求到达时,如果队列已满,客户端可能会收到连接错误(如
ECONNREFUSED)。
返回值:
- 成功时返回
0。 - 失败时返回
-1,并设置errno表示具体错误(如EINVAL、EADDRINUSE等)。
关键特性:
- 1.被动套接字 :调用后,
sockfd不再用于发起连接,而是用于接受连接。 - 2.连接队列 :内核维护一个已完成连接队列(通过
accept()获取)和一个待处理连接队列(由backlog限制)。 - 3.协议支持 :仅对
SOCK_STREAM(TCP)和SOCK_SEQPACKET类型的套接字有效,对SOCK_DGRAM(UDP)无效。
1.2 获取新连接-accept
c
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
功能 :accept() 是一个系统调用函数,用于从监听套接字的连接队列中取出一个已完成的连接,并返回一个新的套接字描述符用于与客户端通信。
参数说明:
- sockfd :已通过
listen()设置为监听状态的套接字描述符。 - addr :(可选)指向
struct sockaddr结构的指针,用于存储客户端的地址信息。如果为NULL,则不获取地址。 - addrlen :(可选)指向
socklen_t的指针,用于指定addr缓冲区的大小(输入),并返回实际地址长度(输出)。如果addr为NULL,则此参数应设为0。
返回值:
- 成功时,返回一个新的套接字描述符,用于与客户端通信。
- 失败时,返回
-1,并设置errno(如EAGAIN、EWOULDBLOCK等)。
关键特性:
- 阻塞行为 :默认情况下,
accept()会阻塞直到有连接到达。若套接字设置为非阻塞(如fcntl设置O_NONBLOCK),则在没有连接时立即返回-1并设置EAGAIN或EWOULDBLOCK。 - 新套接字 :返回的套接字描述符是全新的,与原始监听套接字
sockfd无关。原始sockfd继续监听更多连接。 - 地址信息 :若提供
addr和addrlen,可获取客户端的 IP 和端口,用于日志或访问控制。
1.3 发起新链接-connect
c
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能 :connect() 是一个系统调用函数,用于客户端发起与服务器的连接。它尝试建立一个到指定地址和端口的连接,对于 TCP 套接字,这将完成三次握手过程。
参数说明:
- sockfd :客户端通过
socket()创建的套接字描述符。 - addr :指向目标服务器地址结构的指针(如
struct sockaddr_in用于 IPv4)。 - addrlen:地址结构的大小(以字节为单位)。
返回值:
- 成功时返回
0(连接已建立或已排队)。 - 失败时返回
-1,并设置errno表示具体错误。
关键特性:
-
阻塞行为 :默认情况下,
connect()会阻塞直到连接成功建立或失败(超时)。对于非阻塞套接字(使用fcntl设置O_NONBLOCK),connect()会立即返回-1并设置errno为EINPROGRESS,表示连接正在后台进行,需要后续检查连接状态。 -
不同协议的行为:
-
TCP(SOCK_STREAM):执行三次握手,建立可靠连接。
-
UDP(SOCK_DGRAM) :不实际建立连接,但会将地址信息关联到套接字,后续
send()/recv()可以省略地址参数。 -
本地套接字(AF_UNIX):连接到本地文件系统上的套接字。
-
-
错误处理:
常见错误码:
ECONNREFUSED:服务器拒绝连接(无监听进程或队列满)ETIMEDOUT:连接超时ENETUNREACH:网络不可达EALREADY:套接字已有正在进行的连接(非阻塞模式)
-
与
bind()的关系:客户端通常不需要显式调用
bind(),系统会自动分配一个临时端口(通常称为"ephemeral port")。如果需要指定客户端端口,可以先bind()再connect()。
1.4 从套接字中读取信息-recv
cpp
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能 :recv() 是一个系统调用函数,用于从已连接的套接字(TCP)或已绑定地址的套接字(UDP)接收数据。它是网络编程中数据接收的核心函数。
参数说明:
- sockfd:已连接的套接字描述符(TCP)或已绑定地址的套接字(UDP)。
- buf:指向存储接收数据的缓冲区的指针。
- len:缓冲区的最大长度(以字节为单位)。
- flags:接收操作的标志,用于控制行为(可为0或按位或组合多个标志)。
返回值:
- >0:实际接收的字节数。
- 0:对端关闭了连接(对于TCP,表示连接已正常关闭)。
- -1 :发生错误,
errno被设置为具体错误码(如EAGAIN、EWOULDBLOCK等)。
关键特性:
- 阻塞行为 :默认情况下,
recv()会阻塞直到有数据到达或连接关闭。如果套接字设置为非阻塞(fcntl设置O_NONBLOCK),且没有数据可用,则立即返回-1并设置errno为EAGAIN或EWOULDBLOCK。 - 协议差异
- TCP(SOCK_STREAM) :提供可靠、有序、无边界的数据流。
recv()返回的字节数可能小于请求的长度(len),因为TCP是流式协议。 - UDP(SOCK_DGRAM) :提供不可靠、有边界的数据报。每次调用
recv()通常返回一个完整的数据报(如果缓冲区足够大),否则数据会被截断(部分数据丢失)。
- TCP(SOCK_STREAM) :提供可靠、有序、无边界的数据流。
- 接收模式
- 标准接收 :
recv(sockfd, buf, len, 0),阻塞或非阻塞模式。 - 窥视数据 :使用
MSG_PEEK标志,读取数据但不从接收队列中移除。 - 等待数据 :使用
MSG_WAITALL标志,阻塞直到接收到指定长度的数据(TCP)或一个完整数据报(UDP)。
- 标准接收 :
标志(flags)选项:
| 标志 | 描述 |
|---|---|
0 |
默认行为,阻塞接收 |
MSG_DONTWAIT |
非阻塞模式,等效于设置套接字非阻塞 |
MSG_PEEK |
查看数据但不从队列中移除 |
MSG_WAITALL |
阻塞直到接收到指定长度的数据(TCP)或一个完整数据报(UDP) |
MSG_OOB |
接收带外数据(紧急数据,TCP) |
MSG_TRUNC |
对于数据报套接字,即使数据被截断也返回实际长度 |
MSG_ERRQUEUE |
接收错误队列中的数据(需要 SOL_SOCKET 层) |
1.5 向套接字发送信息-send
C
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能 :send() 是一个系统调用函数,用于向已连接的套接字(TCP)或已指定地址的套接字(UDP)发送数据。它是网络编程中数据发送的核心函数。
参数说明:
- sockfd:已连接的套接字描述符(TCP)或已绑定地址的套接字(UDP)。
- buf:指向要发送数据的缓冲区的指针。
- len:要发送数据的长度(以字节为单位)。
- flags:发送操作的标志,用于控制行为(可为0或按位或组合多个标志)。
返回值:
- >0 :实际发送的字节数(可能小于请求的长度
len)。 - -1 :发生错误,
errno被设置为具体错误码(如EAGAIN、EWOULDBLOCK、EPIPE等)。
关键特性
-
阻塞行为 :默认情况下,
send()会阻塞直到数据被发送到内核发送缓冲区(对于TCP)或发送完成(对于UDP)。如果套接字设置为非阻塞(fcntl设置O_NONBLOCK),且内核发送缓冲区已满,则立即返回-1并设置errno为EAGAIN或EWOULDBLOCK。 -
协议差异
- TCP(SOCK_STREAM) :提供可靠、有序的数据流。
send()将数据放入内核发送缓冲区,由TCP协议栈负责传输。返回值可能小于len,表示部分数据已放入缓冲区。 - UDP(SOCK_DGRAM) :提供不可靠、有边界的数据报。
send()通常将整个数据报一次性发送(如果len不超过最大数据报大小)。返回值等于len表示成功发送。
- TCP(SOCK_STREAM) :提供可靠、有序的数据流。
-
发送模式
- 标准发送 :
send(sockfd, buf, len, 0),阻塞或非阻塞模式。 - 快速发送 :使用
MSG_DONTWAIT标志,非阻塞模式。 - 发送带外数据 :使用
MSG_OOB标志(仅TCP)。
- 标准发送 :
-
连接状态
- 对于TCP,如果对端关闭了连接,
send()可能会触发SIGPIPE信号(默认终止进程),并返回-1且errno为EPIPE。使用MSG_NOSIGNAL标志可以避免信号。 - 对于UDP,发送到未连接的套接字需要指定地址(使用
sendto()),但已连接的UDP套接字可以使用send()。
- 对于TCP,如果对端关闭了连接,
标志(flags)选项
| 标志 | 描述 |
|---|---|
0 |
默认行为,阻塞发送 |
MSG_DONTWAIT |
非阻塞模式,等效于设置套接字非阻塞 |
MSG_NOSIGNAL |
当对端关闭连接时,不发送 SIGPIPE 信号 |
MSG_OOB |
发送带外数据(紧急数据,TCP) |
MSG_CONFIRM |
仅用于UDP,请求确认(已弃用,由内核自动处理) |
MSG_MORE |
表示还有更多数据要发送(Linux特有,用于TCP) |
MSG_NOSIGNAL |
防止对端关闭时产生 SIGPIPE 信号 |
MSG_FASTOPEN |
用于TCP快速打开(需要内核支持) |
1.6 执行shell命令-popen
c
#include <stdio.h>
FILE *popen(const char *command, const char *type);
功能 :popen() 是一个标准库函数,用于执行一个 shell 命令并创建一个管道,允许与该命令的标准输入、输出或错误流进行交互。
参数说明:
- command:要执行的 shell 命令字符串。
- type :指定管道模式,可以是:
"r":从命令的标准输出读取数据"w":向命令的标准输入写入数据"r+":可读写(不常用)"w+":可读写(不常用)
返回值:
- 成功:返回指向 FILE 流的指针,用于与命令的输入/输出交互(该指针需要使用pclose关闭)。
- 失败 :返回
NULL,并设置errno(如EMFILE、EINVAL等)。
关键特性:
- 执行环境
popen()通过 shell(通常是/bin/sh)执行命令,因此可以使用 shell 语法(管道、重定向等)。- 命令字符串可以包含 shell 特殊字符,如
|、>、<、$等。
- 管道创建
- 对于
"r"模式:创建一个管道,命令的标准输出连接到返回的 FILE 流。 - 对于
"w"模式:创建一个管道,命令的标准输入连接到返回的 FILE 流。
- 对于
- 进程管理
popen()会 fork 一个子进程来执行命令。- 必须使用
pclose()关闭 FILE 流并等待子进程结束,否则会产生僵尸进程。
2. 前提头文件
下面代码中会使用我们之前文章中实现过的一些头文件。
2.1 nocopy.hpp
以nocopy为基类的文件无法被拷贝,无法被复制。
cpp
#pragma once
// 不允许派生类对象进行拷贝操作
class nocopy
{
public:
nocopy() {}
~nocopy() {}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
2.2 LockGura.hpp
RAII方式加锁,出作用域即释放。
cpp
#pragma once
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t *_mutex;
};
2.3 Log.hpp
日志头文件,格式化输出日志,方便调试和观察程序运行。
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <cstdarg>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"
namespace log_ns
{
enum
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetTime()
{
time_t now = time(nullptr);
struct tm *curr_time = localtime(&now);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
curr_time->tm_year + 1900,
curr_time->tm_mon + 1,
curr_time->tm_mday,
curr_time->tm_hour,
curr_time->tm_min,
curr_time->tm_sec);
return buffer;
}
class logmessage
{
public:
std::string _level;
pid_t _pid;
std::string _filename;
int _line;
std::string _time;
std::string _message;
};
#define SCREEN_TYPE 1
#define FILE_TYPE 2
const std::string glogfile = "./log.txt";
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
class Log
{
public:
Log(const std::string &logfile = glogfile) : _type(SCREEN_TYPE), _logfile(logfile)
{
}
void Enable(int type)
{
_type = type;
}
void FlushLogToScreen(const logmessage &lg)
{
printf("[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._pid,
lg._filename.c_str(),
lg._line,
lg._time.c_str(),
lg._message.c_str());
}
void FlushLogToFile(const logmessage &lg)
{
std::ofstream out(_logfile, std::ios::app);
if (!out.is_open())
return;
char logtxt[2048];
snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._pid,
lg._filename.c_str(),
lg._line,
lg._time.c_str(),
lg._message.c_str());
out.write(logtxt, strlen(logtxt));
out.close();
}
void FlushLog(const logmessage &lg)
{
LockGuard LockGuard(&glock); // 加锁,为了防止多个线程同时打印日志造成输出信息错乱。
switch (_type)
{
case SCREEN_TYPE:
FlushLogToScreen(lg);
break;
case FILE_TYPE:
FlushLogToFile(lg);
break;
}
}
void logMessage(std::string filename, int line, int level, const char *format, ...)
{
logmessage lg;
lg._level = LevelToString(level);
lg._pid = getpid();
lg._filename = filename;
lg._line = line;
lg._time = GetTime();
va_list ap;
va_start(ap, format);
char log_msg[1024];
vsnprintf(log_msg, sizeof(log_msg), format, ap);
va_end(ap);
lg._message = log_msg;
// print log
FlushLog(lg);
}
~Log()
{
}
private:
int _type;
std::string _logfile;
};
Log lg;
#define EnableScreen() \
do \
{ \
lg.Enable(SCREEN_TYPE); \
} while (0)
#define EnableFile() \
do \
{ \
lg.Enable(FILE_TYPE); \
} while (0)
#define LOG(Level, Format, ...) \
do \
{ \
lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
} while (0)
}
2.4 Thread.hpp
对线程进行封装,方便使用。
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "Log.hpp"
using namespace log_ns;
namespace ThreadMoudle
{
using func_t = std::function<void(const std::string&)>;
class Thread
{
public:
void Excute()
{
LOG(DEBUG, "%s, is running\n", _name.c_str());
_isrunning = true;
_func(_name);
_isrunning = false;
}
public:
Thread(const std::string &name, func_t func):_name(name),_func(func)
{
LOG(DEBUG, "create %s done\n", name.c_str());
}
static void *ThreadRoutine(void *args)
{
Thread *self = static_cast<Thread*>(args);
self->Excute();
return nullptr;
}
bool Start()
{
int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
if (n != 0) return false;
return true;
}
std::string Status()
{
if (_isrunning) return "running";
return "sleep";
}
void Stop()
{
if (_isrunning)
{
::pthread_cancel(_tid);
_isrunning = false;
LOG(DEBUG, "%s, stop\n", _name.c_str());
}
}
void Join()
{
if (!_isrunning)
{
::pthread_join(_tid, nullptr);
LOG(DEBUG, "%s, joined\n", _name.c_str());
}
}
std::string Name()
{
return _name;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func;
};
}
2.5 ThreadPool.hpp
单例模式的线程池。
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <functional>
#include <queue>
#include "Thread.hpp"
#include "LockGuard.hpp"
using namespace ThreadMoudle;
static const int gdefaultnum = 5;
template<typename T>
class ThreadPool
{
private:
void LockQueue()
{
pthread_mutex_lock(&_mutex);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_mutex);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
void WakeupAll()
{
pthread_cond_broadcast(&_cond);
}
bool IsEmpty()
{
return _task_queue.empty();
}
void Sleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
void HandlerTask(const std::string &name)
{
while (true)
{
// 获取任务
LockQueue();
// 任务列表为空,且主线程没有调用Stop
while (IsEmpty() && _isrunning)
{
_sleep_thread_num++;
LOG(DEBUG, "%s, sleep begin\n", name.c_str());
Sleep();
LOG(DEBUG, "%s, sleep end\n", name.c_str());
_sleep_thread_num--;
}
// 任务列表为空,且主线程已调用Stop,线程解锁并退出
if (IsEmpty() && !_isrunning)
{
LOG(DEBUG, "%s, quit\n", name.c_str());
UnlockQueue();
break;
}
// 任务列表不为空,无论主线程是否调用Stop,都要把剩余任务执行完
LOG(DEBUG, "%s execute the task\n", name.c_str());
T t = _task_queue.front();
_task_queue.pop();
UnlockQueue();
// 执行任务
t();
}
}
ThreadPool(int thread_num = gdefaultnum):_thread_num(thread_num), _isrunning(false), _sleep_thread_num(0)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool<T> &) = delete;
void operator= (const ThreadPool<T> &) = delete;
void Init()
{
func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
for (int i = 0; i < _thread_num; i++)
{
std::string threadname = "thread-" + std::to_string(i + 1);
_threads.emplace_back(threadname, func);
}
}
void Start()
{
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
}
}
public:
void Stop()
{
LockQueue();
_isrunning = false;
WakeupAll();
UnlockQueue();
}
static ThreadPool<T> *GetInstance()
{
if (_tp == nullptr)
{
LockGuard lockguard(&_sig_mutex);
if (_tp == nullptr)
{
_tp = new ThreadPool();
_tp->Init();
_tp->Start();
}
}
return _tp;
}
void Equeue(const T &in)
{
LockQueue();
if (_isrunning) // 只有处于运行状态,才可以添加任务。
{
_task_queue.push(in);
if (_sleep_thread_num) Wakeup();
}
UnlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
private:
int _thread_num;
std::vector<Thread> _threads;
std::queue<T> _task_queue;
bool _isrunning;
int _sleep_thread_num;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
// 单例模式
static ThreadPool<T> *_tp;
static pthread_mutex_t _sig_mutex;
};
template<typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
template<typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
2.6 InetAddr.hpp
对struct sockaddr_in类进行封装,方便获取和使用。
cpp
#pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
class InetAddr
{
private:
void ToHost(const struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
_ip = inet_ntoa(addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in &addr):_addr(addr)
{
ToHost(addr);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
std::string AddrStr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
3. V1 - TCP Echo Server - 远程回显信息
这里我们以tcp的方式实现对客户端发来的信息进行回显。
其中包括四个版本:缺陷版、多进程版、多线程版、线程池版,具体在TcpServer类中有注释说明。
3.1 TcpServer.hpp
封装TcpServer类,负责获取客户端的新连接,然后将新的连接进行处理。
cpp
#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "InetAddr.hpp"
#include "nocopy.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
using namespace log_ns;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
LISTEN_ERROR
};
const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;
using task_t = std::function<void()>;
class TcpServer : public nocopy
{
public:
struct ThreadData
{
ThreadData(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr)
{}
int _sockfd;
TcpServer* _self;
InetAddr _addr;
};
TcpServer(uint16_t port = gport): _port(port), _listensockfd(gsock), _isrunning(false)
{}
~TcpServer(){}
void Init()
{
// 1.创建socket
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(FATAL, "socket create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd = %d\n", _listensockfd);
// 2.bind socket和addr
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;
if (::bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
{
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
LOG(INFO, "bind success\n");
// 3.tcp是面向连接的,tcp需要未来不断的能够获取连接
if (::listen(_listensockfd, gblcklog) < 0)
{
LOG(FATAL, "listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO, "listen success\n");
}
void Service(int sockfd, InetAddr addr)
{
// 长服务
while(true)
{
// TCP是面向字节流的,这里类似于读写文件,可以直接使用read/write进行读写
char inbuffer[1024];
ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = '\0';
LOG(INFO, "client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);
std::string echo_string = "[server echo] # ";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
break;
}
else
{
LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
break;
}
}
::close(sockfd);
}
static void* Execute(void* args)
{
pthread_detach(pthread_self());// 线程分离,无需被join
ThreadData* td = static_cast<ThreadData*>(args);
td->_self->Service(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = ::accept(_listensockfd, (struct sockaddr*)&client, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
continue;
}
InetAddr addr(client);
LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", addr.AddrStr().c_str(), sockfd);
// 2.进行通信
// V1 --- 缺陷版 --- 一次只能和一个客户端建立连接,无并发处理能力
// Service(sockfd, addr);
// V2 --- 多进程版 --- 具备并发处理能力,但开销较大,效率较低
// pid_t id = ::fork();
// if (id == 0)
// {
// // child
// ::close(_listensockfd); // 建议关闭无用文件描述符,防止误操作
// // 让父进程不必等待子进程也可以屏蔽SIGCHLD信号(建议),或者使用非阻塞轮询方案,这两个方案之前使用过,这里介绍一个新的方案
// // 子进程创建孙子进程,然后子进程退出,马上被父进程等待,孙子进程变成孤儿进程,被bash领养
// // 从此以后孙子进程即使执行完,也不需要父进程等待了,OS会自行回收
// if (fork() > 0) exit(0);
// // 这里实际上是孙子进程再执行
// Service(sockfd, addr);
// exit(0);
// }
// // father
// ::close(sockfd); // 必须关闭,否则会导致新连接一直在打开但是没关闭,会导致文件描述符泄露,并且可以防止被后面的子进程继承。
// int n = ::waitpid(id, nullptr, 0);
// if (n > 0)
// {
// LOG(INFO, "wait child success\n");
// }
// V3 -- 多线程版 --- 主线程不能关闭文件描述符,线程共享文件描述符
pthread_t tid;
ThreadData* td = new ThreadData(sockfd, this, addr);
pthread_create(&tid, nullptr, Execute, td);
// V4 -- 线程池版 --- 像这种长服务并不适合使用线程池,因为线程池的线程数量是有限的,如果用户数量超过线程池中的线程上限,会出问题
// 因此,还是推荐使用上一个版本,这里只是为了熟悉代码而实现一份
// task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
// ThreadPool<task_t>::GetInstance()->Equeue(t);
}
_isrunning = false;
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
3.2 ServerMain.cc
服务端框架,创建TcpServer对象,并运行。
cpp
#include <memory>
#include "TcpServer.hpp"
// ./server 8888
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Start();
return 0;
}
3.3 ClientMain.cc
客户端逻辑,创建套接字,向服务器发送信息,接收信息并回显。
cpp
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace log_ns;
enum
{
SOCKET_ERROR = 1,
CONNECT_ERROR
};
// ./client server-ip server-port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1.创建socket
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
LOG(FATAL, "create socket error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd = %d\n", sockfd);
// 2.不需要显示bind,但是一定要都自己的ip和port,隐式bind,OS会自动bind,用自己的ip和随机端口号
// 3.向指定服务器发起连接
struct sockaddr_in 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, (struct sockaddr*)&server, sizeof(server));
if (n < 0)
{
LOG(FATAL, "connect error\n");
exit(CONNECT_ERROR);
}
LOG(INFO, "connect success\n");
while(true)
{
std::string message;
std::cout << "Enter # ";
std::getline(std::cin, message);
write(sockfd, message.c_str(), message.size());
char echo_buffer[1024];
n = read(sockfd, echo_buffer, sizeof(echo_buffer) - 1);
if (n > 0)
{
echo_buffer[n] = '\0';
std::cout << echo_buffer << std::endl;
}
else
{
LOG(FATAL, "read error\n");
break;
}
}
::close(sockfd);
return 0;
}
3.4 运行展示

4. V2 - TCP Command Server - 远程命令执行
这里我们希望服务端接收到客户端发送的命令,直接进行执行,然后将结果返回给客户端。
对于客户端,我们给一个命令白名单,只有白名单里的命令运行执行。
【注意】对于下面没有提到改造的头文件,直接使用上面的即可。
4.1 Command.hpp
对于TcpServer,我们只希望它接受新连接,不处理消息,而是将消息交给外部处理,让接收消息和处理消息进行解耦,这里封装Command类专门来进行输入命令的处理。
cpp
#pragma once
#include <iostream>
#include <string>
#include <set>
#include <cstdio>
#include <cstring>
#include "InetAddr.hpp"
#include "Log.hpp"
class Command
{
public:
Command()
{
// 命令白名单,也可以读取配置文件,这里为了方便就直接写了
_safe_command.insert("ls");
_safe_command.insert("pwd");
_safe_command.insert("which");
_safe_command.insert("touch");
}
~Command(){}
bool SafeCheck(const std::string cmdstr) // 简单的安全检查,但是还是存在漏洞,这里就不完善了,否则会非常复杂
{
int pos = 0;
while (pos < cmdstr.size() && cmdstr[pos] != ' ') pos++;
return _safe_command.count(std::string(cmdstr.begin(), cmdstr.begin() + pos));
}
std::string Execute(const std::string& cmdstr)
{
std::string result;
FILE* fp = popen(cmdstr.c_str(), "r");
if (fp)
{
char line[1024];
while (fgets(line, sizeof(line), fp)) result += line;
return result.empty() ? cmdstr + " execute success" : result; // 对于没有返回结果的命令返回执行成功提示
}
else
{
return "execute error";
}
}
void HandlerCommand(int sockfd, InetAddr addr)
{
while(true)
{
char commandbuf[1024];
ssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0);
if (n > 0)
{
commandbuf[n] = '\0';
LOG(INFO, "client %s, command: %s\n", addr.AddrStr().c_str(), commandbuf);
std::string result = Execute(commandbuf);
::send(sockfd, result.c_str(), result.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
break;
}
else
{
LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
break;
}
}
}
private:
std::set<std::string> _safe_command;
};
4.2 TcpServer.hpp
改造TcpServer,使处理消息与之解耦。
cpp
#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "InetAddr.hpp"
#include "nocopy.hpp"
#include "Log.hpp"
using namespace log_ns;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
LISTEN_ERROR
};
const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;
using command_service_t = std::function<void(int sockfd, InetAddr addr)>;
class TcpServer : public nocopy
{
public:
struct ThreadData
{
ThreadData(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr)
{}
int _sockfd;
TcpServer* _self;
InetAddr _addr;
};
TcpServer( command_service_t service, uint16_t port = gport): _port(port), _listensockfd(gsock), _isrunning(false), _service(service)
{}
~TcpServer(){}
void Init()
{
// 1.创建socket
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(FATAL, "socket create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd = %d\n", _listensockfd);
// 2.bind socket和addr
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;
if (::bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
{
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
LOG(INFO, "bind success\n");
// 3.tcp是面向连接的,tcp需要未来不断的能够获取连接
if (::listen(_listensockfd, gblcklog) < 0)
{
LOG(FATAL, "listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO, "listen success\n");
}
static void* Execute(void* args)
{
pthread_detach(pthread_self());// 线程分离,无需被join
ThreadData* td = static_cast<ThreadData*>(args);
td->_self->_service(td->_sockfd, td->_addr);
::close(td->_sockfd);
delete td;
return nullptr;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = ::accept(_listensockfd, (struct sockaddr*)&client, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error\n");
continue;
}
InetAddr addr(client);
LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", addr.AddrStr().c_str(), sockfd);
// 2.进行通信
pthread_t tid;
ThreadData* td = new ThreadData(sockfd, this, addr);
pthread_create(&tid, nullptr, Execute, td);
}
_isrunning = false;
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
command_service_t _service;
};
4.3 ServerMain.cc
将Commcand中处理命令的接口进行bind并传到TcpServer中。
cpp
#include <memory>
#include "TcpServer.hpp"
#include "Command.hpp"
// ./server 8888
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
Command cmdservice;
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2), port);
tsvr->Init();
tsvr->Start();
return 0;
}
4.4 运行展示
