C++网络:TCP点对点通信-附带文件传输代码实例

//!
//! 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;
}

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值