//!
//! C++网络:TCP点对点通信-附带文件传输代码实例
//!
//! ===== TCP简介 =====
//! TCP作为传输层通信协议,因为存在三次握手四次挥手等保证了数据的可靠性,
//! 已经成为了目前主流的通信手段。
//!
//! TCP连接的建立需要调用固定函数流程,实现服务器与客户端的连接,
//! 通常这些调用是固定的,连接成功之后服务器与客户端都会拿到套接字,
//! 套接字sock与文件描述符fd的用户几乎是一致的,
//! 支持read,write,close等文件操作函数,
//! 其中套接字sock额外支持网络读写的recv和send函数
//! ===== TCP简介 =====
//!
//!
//! ===== TCP连接 =====
//! 服务器:
//! socket(创建套接字),bind(绑定端口号),accept(等待客户端连接)
//!
//! 客户端:
//! socket(创建套接字),connect(主动连接服务器)
//!
//! 发送与接收:
//! write/send : 将buf写入缓冲区,等待TCP发送,send的区别为添加第四参数
//! read/recv : 从套接字缓冲区内读内容到buf并返回读取字节,recv的区别为添加第四参数
//!
//! 第四参数的通常用处:
//! 1.不查路由:在本机发送数据时提高速度
//! 2.保持缓存:套接字读取buf时不会清除缓冲区,可以反复读取,用于查看
//! 3.等待内容:套接字读取是不会提前返回,等待发送方的字节同步,可用于保证发送与接收次数一致
//! 4.立即返回:非堵塞模式,可以快速响应读写后的操作
//! 5.紧急数据:额外发送一字节的外带数据,且立即发送,基本是鸡肋功能
//!
//! send/recv提供第四参数附带的功能,如果不需要这些功能也不希望提高复杂度,
//! 可将sock看做fd即可,对网络数据的操作可以像对文件的操作一样简单
//! ===== TCP连接 =====
//!
//!
//! ===== 任务简介 =====
//! 建立TCP连接点对点通信,实现文字通信与文件传输功能:
//! 点对点通信即一个客户端与一个服务器相连接,一旦连接成功立刻退出等待,
//! 服务器不再与新的客户端建立连接
//! 由于需要发送信息与文件传输两种传输类型,需要定义一个简单协议,
//! 对文字与文件进行区分并做出不同处理
//! ===== 任务简介 =====
//!
//!
//! ===== 代码流程 =====
//! ux_tcp.h : 可实现点对点连接,连接成功立刻返回通信管道
//! ux_protocol.h : 简单的文件传输机制,可实现传输功能,但不可用,没有对错误与发送超时处理
//! ux_server main : 服务器代码
//! ux_client main : 客户端代码
//! ===== 代码流程 =====
//!
//! 结束语:
//! 本次实现的TCP连接只考虑了点对点,因为是点对点连接,
//! 服务器与客户端都采用了连接后立刻返回的处理,
//! 这是为了减少二者的使用差距,意图在于建立一种两个进程的数据管道,
//! 而不是为了区分服务器与客户端的请求响应机制
//! 如果希望服务器一对多,可以使用IO复用技术,另一篇文章有介绍
//!
//!
//! ux_tcp.h
//!
#ifndef UX_TCP_H
#define UX_TCP_H
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <future>
#include <functional>
#include <sstream>
#include <iostream>
using namespace std;
using namespace std::placeholders;
//===== 日志宏 =====
#define vv(value) "["#value": "<<value<<"] "
#define vloge(...) std::cout<<"\033[31m[Err] ["<<__FILE__<<":<"<<__LINE__ \
<<">] <<<< "<<__VA_ARGS__<<"\033[0m"<<endl
#define vlogw(...) std::cout<<"\033[33m[War] ["<<__FILE__<<":<"<<__LINE__ \
<<">] <<<< "<<__VA_ARGS__<<"\033[0m"<<endl
#define vlogd(...) std::cout<<"\033[32m[Deb] ["<<__FILE__<<":<"<<__LINE__ \
<<">] <<<< "<<__VA_ARGS__<<"\033[0m"<<endl
#define vlogf(...) std::cout<<"[Inf] ["<<__FILE__<<":<"<<__LINE__ \
<<">] <<<< "<<__VA_ARGS__<<endl
//===== 日志宏 =====
//== 字符串类型转换 ==
template<typename T>
string to_string(const T& t)
{ ostringstream os; os<<t; return os.str(); }
template<typename T>
T from_string(const string& str)
{ T t; istringstream iss(str); iss>>t; return t; }
//== 字符串类型转换 ==
//===== 结构体转换string函数 =====
//结构体转string
// 语法解析:(char*)&ct ,由&ct获取结构体地址,在由该地址(char*)转为char*类型的指针
// 根据string构造函数,参数1:char*地址,参数2:长度,可从地址内存中复制二进制内容
template <class T_ct>
static string ct_s(T_ct ct)
{ return string((char*)&ct,sizeof(T_ct)); }
//string转结构体
// 语法解析:*(T_ct*)str.c_str() ,由str.c_str()从string类获取const char*指针,
// 由const char*指针转为T_ct*指针,再*(T_ct*)从指针中获取值,从而返回值
template <class T_ct>
static T_ct st_c(const string &str)
{ T_ct ct = *(T_ct*)str.c_str(); return ct; }
//===== 结构体转换string函数 =====
//===== 数据管道 =====
class channel
{
public:
channel(int fd) : _fd(fd){}
int get_fd() const { return _fd; }
//发送string字符串,带锁
bool send_msg(const string &msg)
{
unique_lock<mutex> lock(_mutex);
if(send_msg(_fd,msg,NULL) == false)
{ if(close_cb) {close_cb(_fd);} return false; }
else return true;
}
//读取反馈信息--线程启动
void read_string_th(int fd,function<void(string)> read_cb,function<void()> close_cb)
{
size_t all_len = 0;
string all_content;
while(true)
{
char buf[1024];
memset(buf,0,sizeof(buf));
size_t size = read(fd,&buf,sizeof(buf));
if(size <= 0) { if(close_cb){close_cb();} return; }
//加入新内容(可能存在上一次的人剩余信息)
all_len += size;
all_content += string(buf,size);
while(true)
{
//超过八个字节(判断是否能完整读出头部大小)
if(all_len > sizeof(all_len))
{
//解析出ct_msg结构体的信息--长度
size_t con_len = *(size_t*)string(all_content,0,sizeof(con_len)).c_str();
//判断目前剩余量是否大于等于一个包的长度
if((all_len - sizeof(all_len)) >= con_len)
{
//解析的内容
string buf_content(all_content,sizeof(all_len),con_len);
if(read_cb) read_cb(buf_content);//解析出完整包后触发回调
//存放剩余的内容
all_len -= sizeof(all_len) + con_len;
all_content = string(all_content.begin() +
sizeof(all_len) + con_len,all_content.end());
}
else break;
}
else break;
}
}
}
//指定发送N个字节的数据
size_t writen(int sock,const void *buf,size_t len) const
{
size_t all = len;
const char *pos = (const char *)buf;
while (all > 0)
{
size_t res = write (sock,pos,all);
if (res <= 0){ if (errno == EINTR){res = 0;} else{return -1;} }
pos += res; all -= res;
}
return len;
}
//发送string字符串
bool send_msg(int sock,const string &msg,size_t *all)
{
size_t len = msg.size();
string buf;
buf += string((char*)&len,sizeof(len));
buf += msg;
size_t ret = writen(sock,buf.c_str(),buf.size());
if(all != nullptr) *all = ret;
return ret != -1u;
}
function<void(int)> close_cb = nullptr; //发送失败时触发回调--用于服务器
private:
int _fd; //连接套接字
std::mutex _mutex; //互斥锁--发送
};
//===== 数据管道 =====
//===== TCP服务器 =====
class ux_tcp
{
public:
//建立连接
shared_ptr<channel> open_tcp(int port,string *in_ip)
{
int listen = init_port(port);
if(listen < 0) { vloge("init port err"); return nullptr; }
//建立请求,接收客户端的套接字(accept返回之后双方套接字可通信)
struct sockaddr_in client;
socklen_t len = sizeof(client);
int fd = accept(listen,(struct sockaddr *)&client, &len);
close(listen); //断开监听套接字,防止新的客户端连接
//新连接进入
if (fd == -1) { vloge("accept err"); return nullptr; }
if(in_ip) *in_ip = inet_ntoa(client.sin_addr);
_pch = make_shared<channel>(fd);
thread(&channel::read_string_th,_pch,fd,sock_read,
bind(&ux_tcp::close_connect,this)).detach();
return _pch;
}
//关闭连接
void close_connect()
{ close(_pch->get_fd()); if(sock_close) sock_close(); }
function<void()> sock_close = nullptr; //关闭连接
function<void(const string &msg)> sock_read = nullptr; //读取数据
private:
shared_ptr<channel> _pch; //数据管道
//! 初始化监听端口,返回套接字
//! 返回值:
//! -1:socket打开失败
//! -2:bind建立失败
//! sock:返回成功,建立的套接字
//!
int init_port(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0); //设置TCP连接模式
if (sock < 0) { return -1; }
int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); //打开复用
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len); //打开心跳
//设置网络连接模式
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET; //TCP协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //监听所有
servaddr.sin_port = htons(port); //兼容端口
//监听或绑定失败返回错误 | listen函数 参数2:正在连接的队列容量(点对点时为1)
if (bind(sock, (struct sockaddr *)&servaddr,
sizeof(servaddr)) < 0 || listen(sock,1) != 0)
{ close(sock); return -2; }
return sock;
}
};
//===== TCP服务器 =====
//===== TCP客户端 =====
class ux_client
{
public:
//建立连接
shared_ptr<channel> open_connect(const string &ip,int port)
{
int fd = init_connect(ip,port);
if(fd < 0) { return nullptr; }
_pch = make_shared<channel>(fd);
thread(&channel::read_string_th,_pch,fd,sock_read,
bind(&ux_client::close_connect,this)).detach();
return _pch;
}
//关闭连接
void close_connect()
{ close(_pch->get_fd()); if(sock_close) sock_close(); }
function<void()> sock_close = nullptr; //关闭连接
function<void(const string &msg)> sock_read = nullptr; //读取数据
private:
shared_ptr<channel> _pch; //数据管道
//! 网络连接初始化
//! 返回值:
//! -1:socket打开失败
//! -2:IP转换失败
//! -3:connect连接失败
//! sock:返回成功,建立的套接字
//!
int init_connect(const string &ip,int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0); //设置TCP连接模式
if (sock < 0) { return -1; }
int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); //打开复用
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len); //打开心跳
//设置网络连接模式
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET; // TCP协议族
servaddr.sin_port = htons(port); //兼容端口
//IP转换
if(inet_pton(AF_INET,ip.c_str(), &servaddr.sin_addr) <=0 )
{ return -2; }
//建立连接
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
{ return -3; }
return sock;
}
};
//===== TCP客户端 =====
#endif // UX_TCP_H
//!
//! ux_protocol.h
//!
#ifndef UX_PROTOCOL_H
#define UX_PROTOCOL_H
#include "ux_tcp.h"
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
//===== stmv =====
//功能:字符串切割,按分隔符将字符串切割到数组
//算法:利用vector<bool>生成与字符串一样长的标记位
// 切割算法扫描到切割符时将vector<bool>对应标记位置1(切割符占领位)
// 然后将连续0段加入结果数组
//用法示例:
// [1]
// string a = "11--22--33";
// string b = "11--22++33";
// string c = "11 22 33 44++55--66";
// vector<string> vec = vts::stmv(a)("--");
// [ret = 11,22,33]
// vector<string> vec1 = vts::stmv(b)("--");
// [ret = 11,22++33]
// vector<string> vec2 = vts::stmv(c)(" ","++","--");
// [ret = 11,22,33,44,55,66]
//
struct stmv
{
string v_str;
vector<string> vec_flg;
vector<bool> vec_bit;
stmv(const string &str) : v_str(str) { vec_bit.resize(str.size(),false); }
template<class ...Tarr>
vector<string> operator()(const Tarr &...arg) { return push_flg(arg...); }
//获取切割符
template<class ...Tarr> vector<string> push_flg()
{ return split_value(v_str,vec_flg); }
template<class ...Tarr>
vector<string> push_flg(const string &flg,Tarr ...arg)
{ vec_flg.push_back(flg); return push_flg(arg...); };
//根据标记切割字符串
vector<string> split_value(const string &in_str,const vector<string> &in_flg)
{
vector<string> vec;
//标记数循环
for(size_t iflg=0;iflg<in_flg.size();iflg++)
{
//字符串标记排查,存在用bit标记
size_t pos_begin = 0;
while(true)
{
pos_begin = in_str.find(in_flg[iflg],pos_begin);
if(pos_begin != in_str.npos)
{
for(size_t il=0;il<in_flg[iflg].size();il++)
{ vec_bit[pos_begin+il]=1; }
pos_begin+=1;
}
else break;
}
}
//根据0/1状态获取字符串,加入返回结果
string str;
for(size_t i=0;i<vec_bit.size();i++)
{
if(vec_bit[i] == false)
{
if(i>0 && (vec_bit[i-1] == true)) str.clear();
str+=in_str[i];
}
else if(i>0 && (vec_bit[i-1] == false)) vec.push_back(str);
}
//末尾无状态转跳时加入结果
if(vec_bit[vec_bit.size()-1] == false)
{ vec.push_back(str); }
return vec;
}
};
//===== stmv =====
//== 文件传输结构体 ==
struct ct_msg
{
bool is_file; //判断是否为文件(发送文件或者发送信息)
bool is_begin; //首次发送
bool is_end; //发送结束
size_t size_buf; //本次发送的buf真实长度
size_t size_file; //文件总长度,首次发送时附带
char filename[256]; //文件名,首次发送时附带
char buf[4096]; //文件内容或者信息内容
};
//== 文件传输结构体 ==
//== 发送文件 ==
bool send_file(const string &filename,shared_ptr<channel> pch)
{
ct_msg ct;
memset(&ct,0,sizeof(ct));
ct.is_file = true;
fstream ofs(filename,ios::in);
if(ofs.is_open())
{
//偏移到文件末尾获取文件总长度并返回起点
ofs.seekg(0,ios::end);
ct.size_file = ofs.tellg();
ofs.seekg(0,ios::beg);
cout<<"send file: "<<filename<<endl;
//首次发送
ct.is_begin = true;
strncpy(ct.filename,filename.c_str(),sizeof(ct.filename));
pch->send_msg(ct_s<ct_msg>(ct));
//发送文件内容
ct.is_begin = false;
ct.is_end = false;
while(ofs.eof() == false)
{
//读取内容到buf与记录buf字节数
ofs.read(ct.buf,sizeof(ct.buf));
ct.size_buf = ofs.gcount();
pch->send_msg(ct_s<ct_msg>(ct));
}
ofs.close();
//最后一次发送
if(ofs.eof()) { ct.is_end = true; }
pch->send_msg(ct_s<ct_msg>(ct));
}
else return false;
cout<<"send finish: "<<filename<<endl;
return true;
}
//== 发送消息 ==
bool send_txt(const string &str,shared_ptr<channel> pch)
{
ct_msg ct;
memset(&ct,0,sizeof(ct));
ct.is_file = false;
strncpy(ct.buf,str.c_str(),sizeof(ct.buf));
return pch->send_msg(ct_s<ct_msg>(ct));
}
//== 解析传输内容 ==
void parse_msg(const string &msg)
{
ct_msg ct = st_c<ct_msg>(msg);
if(ct.is_file) //如果是文件的处理方式
{
static fstream ofs;
//首次接收
if(ct.is_begin)
{
cout<<"begin recv file: "<<ct.filename<<endl;
ofs.open(string(ct.filename),ios::out);
if(ofs.is_open() == false) { vlogw("== open err =="); }
return; //提前返回
}
//最后一次
if(ct.is_end)
{
cout<<"recv file finish: "<<ct.filename<<endl;
ofs.close();
return; //提前返回
}
if(ofs.is_open()) { ofs.write(ct.buf,ct.size_buf); } //发送中
}
else { cout<<"read: "<<ct.buf<<endl; } //信息的处理方式--打印
}
//== 解析命令 ==
bool parse_cmd(const string &cmd,shared_ptr<channel> pch)
{
ct_msg ct;
memset(&ct,0,sizeof(ct));
vector<string> vec = stmv(cmd)(":"); //解析出分割符内容
if(vec.size() < 2) { return send_txt(cmd,pch); } //发送信息
else if(vec[0] == "file") { return send_file(vec[1],pch); } //发送文件
else return false;
}
#endif // UX_PROTOCOL_H
//!
//! ux_server.h : main
//!
#include "ux_tcp.h"
#include "ux_protocol.h"
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
int main()
{
const int port = 5005;
string ip;
bool is_run = true;
ux_tcp server;
server.sock_close = [&](){
cout<<"sock_close"<<endl;
is_run = false;
};
server.sock_read = [=](const string &msg){
parse_msg(msg);
};
cout<<"server: 5005"<<endl;
auto sock = server.open_tcp(port,&ip);
if(sock == nullptr) { cout<<"open_tcp err"<<endl; return -1; }
cout<<"connect: in "<<ip<<endl;
while (is_run)
{
string str;
cin>>str;
if(str == "exit" || is_run == false) break;
is_run = parse_cmd(str,sock);
}
cout<<"===== end ====="<<endl;
return 0;
}
/*
* 对话测试:
*
//== 服务端 ==
server: 5005
connect: in 127.0.0.1
HellowAmy
read: 你好
我发一份软件给你,记得处理
file:qtapp.run
send file: qtapp.run
send finish: qtapp.run
read: 收到了
OK
//== 客户端 ==
client: 127.0.0.1 | 5005
connect: in
read: HellowAmy
你好
read: 我发一份软件给你,记得处理
begin recv file: qtapp.run
recv file finish: qtapp.run
收到了
read: OK
sock_close
*/
//!
//! ux_client.h : main
//!
#include "../ux_server/ux_tcp.h"
#include "../ux_server/ux_protocol.h"
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
int main()
{
const string ip = "127.0.0.1";
const int port = 5005;
bool is_run = true;
ux_client client;
client.sock_close = [&](){
cout<<"sock_close"<<endl;
is_run = false;
};
client.sock_read = [=](const string &msg){
parse_msg(msg);
};
cout<<"client: "<<ip<<" | "<<port<<endl;
auto sock = client.open_connect(ip,port);
if(sock == nullptr) { cout<<"open_tcp err"<<endl; return -1; }
cout<<"connect: in"<<endl;
while (is_run)
{
string str;
cin>>str;
if(str == "exit" || is_run == false) break;
is_run = parse_cmd(str,sock);
}
cout<<"===== end ====="<<endl;
return 0;
}