🌈 个人主页:Zfox_
🔥 系列专栏:Linux
目录
- 一:🔥 Socket 编程 TCP
- 🦋 TCP socket API 详解
- 🦋 多线程远程命令执行
- 🦋 网络版计算器(应用层自定义协议与序列化)
- 二:🔥 共勉
一:🔥 Socket 编程 TCP
🦋 TCP socket API 详解
下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中
socket
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
domain: 域 / 协议家族
AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols
type: 报文类型
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
protocol: 传输层类型
默认为0
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
bind
#include <sys/types.h>
#include <sys/socket.h>
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
// 2. 填充网络信息,并bind绑定
// 2.1 没有把socket信息设置进入内核
struct sockaddr_in local;
bzero(&local, sizeof(local)); // string.h
local.sin_family = AF_INET;
local.sin_port = ::htons(_port); // 要被发送给对方,既要发送到网络中! 主机序列转换为网络序列 大小端转换 网络中都是大端 #include <arpa/inet.h>
local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip -> 4bytes 2. network order #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
local.sin_addr.s_addr = INADDR_ANY;
// 2.1 bind 这里设置进入内核
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
📚 我们之前在调用socket的时候,明明已经填充了一次 AF_INET
, 为什么这里还需要一次呢?
创建套接字的时候填充的 AF_INET 是给操作系统文件系统里的网络文件接口,告诉我们的操作系统我们要创建一个网络的套接字。
这里则是用来填充 sockaddr_in 网络信息,只有套接字的结构和这里的结构一样,操作系统才能绑定成功。
📚 必带四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
listen
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd: 指定的套接字
backlog 等待连接队列的最大长度。
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
listen() 声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大 (一般是 5)
accept
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 指定的套接字
- 三次握手完成后, 服务器调用 accept() 接受连接;
- 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给 addr 参数传 NULL,表示不关心客户端的地址;
- addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 指定的套接字
- 客户端需要调用 connect()连接服务器;
- connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而 connect 的参数是对方的地址;
- connect() 成功返回 0,出错返回-1
🦋 多线程远程命令执行
📚 代码结构
C++
CommandExec.hpp Common.hpp Cond.hpp InetAddr.hpp Log.hpp Makefile
Mutex.hpp TcpClient.cc TcpServer.cc TcpServer.hpp Thread.hpp ThreadPool.hpp
TcpServer.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
static const uint16_t gport = 8080;
using handler_t = std::function<std::string(std::string)>;
#define BACKLOG 8
class TcpServer
{
using task_t = std::function<void()>;
struct ThreadData
{
int sockfd;
TcpServer *self;
};
public:
TcpServer(handler_t handler, int port = gport)
: _handler(handler),
_port(port),
_isrunning(false)
{
}
bool InitServer()
{
// 1. 创建tcp socket
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); // Tcp Socket
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "_listensockfd error";
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "_listensockfd create success, _listensockfd is : " << _listensockfd;
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;
// 2. bind
int n = ::bind(_listensockfd, CONV(&local), sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success, _listensockfd is : " << _listensockfd;
// 3. cs tcp是面向连接的,就要求tcp随时随地等待被连接
// tcp 需要将socket设置成为监听状态
n = ::listen(_listensockfd, BACKLOG);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
Die(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success, _listensockfd is : " << _listensockfd;
//::signal(SIGCHLD, SIG_IGN); // 子进程退出,OS会自动回收资源,不用再wait了
return true;
}
void HandlerRequest(int sockfd) // TCP 也是全双工通信
{
LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;
char inbuffer[4096];
// 长任务
while(true)
{
ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
if(n > 0)
{
LOG(LogLevel::INFO) << inbuffer;
inbuffer[n] = 0;
// std::string echo_str = "server echo# ";
// echo_str += inbuffer;
std::string cmd_result = _handler(inbuffer);
::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0);
}
else if(n == 0)
{
// read 如果读取返回值是0,表示client退出
LOG(LogLevel::INFO) << "client quit: " << sockfd;
break;
}
else {
// 读取失败了
break;
}
}
::close(sockfd); // fd泄露问题
}
static void *ThreadEntry(void *args)
{
pthread_detach(pthread_self());
ThreadData* data = (ThreadData*)args;
data->self->HandlerRequest(data->sockfd);
delete data;
return nullptr;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 不能直接读取数据
// 1. 获取新连接
struct sockaddr_in peer;
socklen_t peerlen = sizeof(peer);
LOG(LogLevel::DEBUG) << "accept ing ...";
// 我们要获取客户端的信息:数据(sockfd) + client socket信息(accept)
int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error" << strerror(errno);
continue;
}
// 获取连接成功了
LOG(LogLevel::INFO) << "accept success, socket 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)
// {
// // child
// // 问题1: 父进程的文件描述符表子进程会继承 父子各一张共两张
// // 1.关闭不需要的fd
// ::close(_listensockfd);
// if(fork() > 0) exit(0); // 子进程退出
// // 孙子进程 -> 孤儿进程 -> 1
// HandlerRequest(sockfd);
// exit(0);
// }
// ::close(sockfd); // 父进程也关闭不需要的 已经交给子进程了
// // 不会阻塞
// pid_t rid = ::waitpid(id, nullptr, 0);
// if(rid < 0)
// {
// LOG(LogLevel::WARNING) << "waitpid error";
// }
// version-2 多线程版本
// pthread_t tid;
// ThreadData* data = new ThreadData;
// data->sockfd = sockfd;
// data->self = this;
// pthread_create(&tid, nullptr, ThreadEntry, data); // 主线程和新线程是如何看待,文件描述符表, 共享一张文件描述符表!!属于同一个进程 !
// version-3 线程池版本 一般用于短任务(注册登录),少量用户
// task_t f = std::bind(&TcpServer::HandlerRequest, this, sockfd); // 构建任务
// ThreadPool<task_t>::getInstance()->Equeue(f);
ThreadPool<task_t>::getInstance()->Equeue([this, sockfd](){
this->HandlerRequest(sockfd);
});
}
}
void Stop()
{
_isrunning = false;
}
~TcpServer()
{
}
private:
int _listensockfd; // 监听socket
uint16_t _port;
bool _isrunning;
// 处理上层任务的入口
handler_t _handler;
};
TcpServer.cc
#include "TcpServer.hpp"
#include "CommandExec.hpp"
#include <memory>
using namespace LogModule;
int main()
{
ENABLE_CONSOLE_LOG();
Command cmd;
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>([&cmd](std::string cmdstr){
return cmd.Execute(cmdstr);
});
tsvr->InitServer();
tsvr->Start();
return 0;
}
TcpClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
// ./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];
int server_port = 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_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
server_addr.sin_addr.s_addr = inet_addr(serverip.c_str());
// client 不需要显示的进行bind, tcp是面向连接的, connect 底层自动会进行bind
int n = ::connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(n < 0)
{
std::cout << "Connect to server failed." << std::endl;
return 3;
}
// echo client
std::string message;
while(true)
{
char inbuffer[1024];
std::cout << "input message: ";
std::getline(std::cin, message);
n = ::write(sockfd, message.c_str(), message.size());
if(n > 0)
{
int m = ::read(sockfd, inbuffer, sizeof(inbuffer));
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
else break;
}
else break;
}
::close(sockfd);
return 0;
}
CommandExec.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#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("who");
_white_list.insert("whoami");
_white_list.insert("ll");
}
bool SafeCheck(const std::string& cmdstr)
{
auto iter = _white_list.find(cmdstr);
return iter == _white_list.end() ? false : true;
}
// 给你一个命令字符串"ls -l",执行它并返回执行结果
std::string Execute(std::string cmdstr)
{
// 1. pope
// 2.fork + dup2(pipe[1], 1) + exec*, 执行结果给父进程, pipe[0]
// 3. return
// FILE *popen(const cahr *command, const char *type);
// pclose(FILE *stream);
if(!SafeCheck(cmdstr))
{
return std::string(cmdstr + "不支持");
}
FILE *fp = popen(cmdstr.c_str(), "r");
if(fp == nullptr)
{
return std::string("Failed");
}
char buffer[line_size];
std::string result;
while(true)
{
char *ret = ::fgets(buffer, sizeof(buffer), fp);
if(!ret) break;
result += ret;
}
pclose(fp);
return result.empty() ? std::string("Done") : result;
}
private:
std::set<std::string> _white_list;
};
CommandExec.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#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("who");
_white_list.insert("whoami");
_white_list.insert("ll");
}
bool SafeCheck(const std::string& cmdstr)
{
auto iter = _white_list.find(cmdstr);
return iter == _white_list.end() ? false : true;
}
// 给你一个命令字符串"ls -l",执行它并返回执行结果
std::string Execute(std::string cmdstr)
{
// 1. pope
// 2.fork + dup2(pipe[1], 1) + exec*, 执行结果给父进程, pipe[0]
// 3. return
// FILE *popen(const cahr *command, const char *type);
// pclose(FILE *stream);
if(!SafeCheck(cmdstr))
{
return std::string(cmdstr + "不支持");
}
FILE *fp = popen(cmdstr.c_str(), "r");
if(fp == nullptr)
{
return std::string("Failed");
}
char buffer[line_size];
std::string result;
while(true)
{
char *ret = ::fgets(buffer, sizeof(buffer), fp);
if(!ret) break;
result += ret;
}
pclose(fp);
return result.empty() ? std::string("Done") : result;
}
private:
std::set<std::string> _white_list;
};
🦋 网络版计算器(应用层自定义协议与序列化)
代码结构
C++
Calculator.hpp Common.hpp Cond.hpp Deamon.hpp InetAddr.hpp Log.hpp Makefile Mutex.hpp
Protocol.hpp TcpClient.cc TcpServer.cc TcpServer.hpp Thread.hpp ThreadPool.hpp
// 简单起见, 可以直接采用自定义线程
// 直接 client<<->>server 通信, 这样可以省去编写没有干货的代码
网络版计算器(应用层自定义协议与序列化)
二:🔥 共勉
以上就是我对 【Linux】Socket编程-TCP构建自己的C++服务器
的理解,想要完整代码可以私信博主噢!觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉