【Linux】TCP网络通信编程

文章目录


前言

本章实现了三个版本基于TCP的服务器以及客户端的实现,还有简单的业务功能。这里的业务处理在哪个版本下都可以实现,并不是指定的版本,我这里方便测试就一个版本实现一个业务功能

三个版本 业务功能 实现工具
单进程版本 简单的回显客户端和服务端的消息 Visual Studio Code (VScode)
多进程版本 实现英译汉的功能 Xshell 8
多线程版本 实现从客户端端输入指令,然后在服务端实现后再返回执行结果

一、单进程版本

这里处理的业务是:简单的回显客户端和服务端的消息

1.代码部分

如上图所以,服务端会显示是谁(ip)发的消息,而客户端也会回显自己发的消息。要想实现以上的功能,首先需要6个文件,分别是:

TcpServer.cc(服务端),TcpClient.cc(客户端),TcpServer.hpp(服务端的开源文件),Common.hpp(禁止拷贝和错误码设置文件),InetAddr.hpp(端口和ip的转换文件),Makefile(自动编译文件)

1.TcpServer.cc(服务端)

cpp 复制代码
#include<iostream>
#include"TcpServer.hpp"
#include<memory>
//正确使用方式
void Usage(std::string line)
{
    std::cout<<line<<" port "<<std::endl;
}
//./tcpserver port
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit( USAGE_ERR);
    }
    
    uint16_t port=std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);  
    
    tsvr->Init();
    tsvr->Run();
}

2.TcpClient.cc(客户端)

cpp 复制代码
#include"Common.hpp"
#include<iostream>
#include"InetAddr.hpp"
//./tcpclient ip port

//判断是否全是空白字符(空格、Tab、换行)
bool AllSpace(const std::string& s)
{
    for (auto c : s)
    {
        if (!isspace(c))return false;
    }
    return true;
}
//提示正确用法            
void Usage(std::string proc)
{
    std::cout<<proc<<" ip  port"<<std::endl;
}

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string _ip=argv[1];
    uint16_t _port=std::stoi(argv[2]);

    //创建套接字
    int _sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(_sockfd<0)
    {
        std::cout<<"_sockfd error"<<std::endl;
        exit(SOCK_ERR);
    }

    //这里也需要绑定,只不过不需要显示绑定,随机方式选择端口号,listen,accept都不需要做,那是服务器的事

    //向目标服务器直接发起建立连接的请求
    InetAddr client(_ip,_port);
    int n=connect(_sockfd,client.NetAddrPtr(),client.InetAddrLen());
    if(n<0)
    {
        std::cout<<"connet error"<<std::endl;
        exit(CONNECT_ERR);
    }

    while(true)
    {
        //发消息
        std::string line;
        std::cout<<"Please Enter#";
        std::getline(std::cin,line);

        //空白字符(空格、Tab、换行)不发送
        if(line.empty()||AllSpace(line))continue;
        
        write(_sockfd,line.c_str(),line.size());

        //收消息
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer)); // 清空缓冲区,避免乱码
        ssize_t s= read(_sockfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            std::cout<<buffer<<std::endl;
        }
        else if(s==0)
        {
           break;
        }
        else break;
    }
    close(_sockfd);
    return 0;
}

3.TcpServer.hpp(服务端的开源文件)

cpp 复制代码
#pragma once 
#include<iostream>
#include"Common.hpp"
#include"InetAddr.hpp"

const static int defaultsockfd =-1;
const static int backlog =253;

class TcpServer:public NoCopy
{
public:
   TcpServer(uint16_t port):_port(port),_listensockfd(defaultsockfd),isrunning(false)
    {}
    void Init()
    {
        //创建监听套接字
        _listensockfd=socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd<0)
        {
            std::cout<<"socket error"<<std::endl;
            exit(SOCK_ERR);
        }
        std::cout<<"sockfd success:"<<_listensockfd<<std::endl;

        //绑定端口和ip
        InetAddr peer(_port);
        int n=bind( _listensockfd,peer.NetAddrPtr(),peer.InetAddrLen());
        if(n<0)
        {
            std::cout<<"bind error"<<std::endl;
            exit(BIND_ERR);
        }
        std::cout<<"bind success:"<<n<<std::endl;

        //监听套接字
        int l=listen(_listensockfd,backlog);
        if(l<0)
        {
            std::cout<<"listen error"<<std::endl;
            exit(LISTEN_ERR);
        }
        std::cout<<"listen success:"<<l<<std::endl;

    }
    void Service(int sockfd,InetAddr&peer)
    {
        while(true)
        {
            //读取客服端的消息
            char buffer[1024];
            ssize_t s=read(sockfd,buffer,sizeof(buffer)-1);//这里是有bug的,这里我们先这样写,后面在做相应解释。
            if(s>0)
            {
                buffer[s]=0;
                std::cout<<peer.StringAddr()<<"say:"<<buffer<<std::endl;     

                //回显给客户端的消息
                std::string line="server echo : ";
                line+=buffer;
                 write(sockfd,line.c_str(),line.size());
            }
        }
    }
    void Run()
    {
        isrunning =true;
        while(isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(sockaddr_in);
            //获取连接并返回一个文件描述符来提供服务,如果没有远端来连接的话,它会一直阻塞在这里。
            int _sockfd=accept( _listensockfd,CONV(peer),&len);
            if(_sockfd<0)
            {
                std::cout<<"accept cerr"<<std::endl;
                continue;
            }

            InetAddr addr(peer);
            std::cout<<"accept success:"<<addr.StringAddr()<<std::endl;

            //这里就是tcp所提供的服务
            Service(_sockfd,addr);
            //服务结束后关闭对应的文件描述符,避免浪费资源
            close(_sockfd);
        }
        isrunning=false;
    
    }
    ~TcpServer()//关闭监听套接字
    {
      if (_listensockfd != defaultsockfd)
        {
            close(_listensockfd);
            _listensockfd = defaultsockfd;
        }
    }
private:
    uint16_t _port;
    int _listensockfd;
    bool isrunning;
};

4.Common.hpp(禁止拷贝和错误码设置文件)

cpp 复制代码
#pragma once
#include<iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum  ExitCode
{
    OK=0,
    USAGE_ERR,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    WRITE_ERR
};

class NoCopy//禁止拷贝,这里实现的直接就是把拷贝构造和赋值构造删除
{
public:
    NoCopy(){}
    NoCopy(const NoCopy&)=delete;
    const NoCopy&operator=(const NoCopy&)=delete;
    ~NoCopy(){}

private:
};

#define  CONV(addr) ((struct sockaddr*)&addr)

5.InetAddr.hpp(端口和ip的转换文件)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<cstring>
class InetAddr
{
public:
 
    InetAddr(const struct sockaddr_in &addr)//网络格式转换成本地格式
    :_addr(addr)
    {
        _port=ntohs(_addr.sin_port);
        _ip=inet_ntoa(_addr.sin_addr);//四字节网络ip风格转换成点分十进制
    }

    InetAddr(uint16_t port)
    :_port(port)
    {
        
        bzero(&_addr,sizeof(_addr));//清空sockaddr_in 
        _addr.sin_family=AF_INET;
        _addr.sin_port=htons(_port);//本地格式转化成网络格式
        _addr.sin_addr.s_addr=INADDR_ANY;
    }
    InetAddr(std::string ip,uint16_t port)
    :_ip(ip)
    ,_port(port)
    {
        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());
    }
    std::string StringAddr()const
    {
        return _ip + ":" + std::to_string(_port);

    }
    bool operator==(const InetAddr&addr)
    {
        return addr._ip==_ip&&addr._port==_port;
    }

    //返回port和ip
    uint16_t port(){return _port;}
    std::string  ip(){return _ip;}
    
    const struct sockaddr_in&NetAddr()const{return  _addr;}

    const struct sockaddr*NetAddrPtr(){return  CONV(_addr);}

    socklen_t InetAddrLen(){return sizeof(_addr);}


    ~InetAddr(){}
private: 
    struct sockaddr_in _addr;
    uint16_t _port;
    std::string _ip;

};

6.Makefile(自动编译文件)

cpp 复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17 
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f tcpclient tcpserver 

2.相关接口

cpp 复制代码
int socket(int domain, int type, int protocol);//创建监听套接字
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //绑定端口和ip             
int listen(int sockfd, int backlog);//监听套接字

ssize_t read(int fd, void *buf, size_t count);//读消息
ssize_t write(int fd, const void *buf, size_t count);//写消息

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接口的参数介绍

socket和bind在UDP哪里已经详细介绍过了,所以这里就不作过多介绍了。

cpp 复制代码
int listen(int sockfd, int backlog)

listen() 是TCP 服务器专用的系统调用 ,作用是将一个套接字从主动连接状态转为被动监听状态,等待客户端发起连接
返回值:成功返回0,失败返回-1,错误码被重新设置

  1. int sockfd :服务器端的套接字文件描述符
    来源 :必须是通过 socket() 创建、并通过 bind() 绑定了 IP + 端口的套接字
    作用 :告诉操作系统:我要监听这个套接字对应的 IP 和端口,等待客户端连接
    要求:只能是TCP 套接字(SOCK_STREAM 类型),UDP 不需要 listen

  2. int backlog

    这是最容易误解的参数,它不是最大客户端连接数,而是TCP 未处理连接队列的长度上限,这里不过多说明,记住常用值128、512、1024(高并发服务器可设大一点)就行了。

cpp 复制代码
ssize_t read(int fd, void *buf, size_t count)

read() 是Linux/Unix 系统最核心的 I/O 函数,作用是从文件 / 套接字 / 设备中读取数据,是通用的读取接口。

  1. int fd :文件描述符,告诉操作系统要从哪个 "文件" 读取数据
    常见值

    0:标准输入(键盘)

    服务器 / 客户端套接字(网络通信)

    普通文件的描述符

  2. void buf:数据缓冲区,void 表示通用指针,可以接收任意类型的内存地址(char[]、自定义结构体等),但是必须是已分配好的有效内存(不能是 NULL,否则程序崩溃)

  3. size_t count :期望读取的最大字节数,告诉操作系统最多往缓冲区里写多少字节数据
    注意 :这是 "期望读取的最大值",不是一定会读到这么多!

  4. 返回值

    大于0:实际成功读取的字节数

    等于0:读到文件末尾(EOF),没有数据了

    等于-1:读取失败,设置 errno 错误码

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);

write() 是 Linux/Unix 系统核心输出函数 ,和 read() 成对使用,作用是把数据写入文件、套接字、终端、管道等一切 "文件"。

参数跟read差不多,只不过buf是写入的数据缓冲区

  1. 返回值

等于 0:实际成功写入的字节数(可能小于你传入的 count)

等于-1:写入失败,会设置 errno

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

这是 TCP 服务器专用函数,作用是:从全连接队列里取出一个已完成三次握手的客户端连接,创建一个全新的套接字专门和这个客户端通信。

  1. int sockfd :监听套接字文件描述符
    来源 :就是你调用 socket() → bind() → listen() 时用的那个套接字
    作用 :告诉操作系统:我要从这个监听套接字的等待队列里取客户端连接
    注意:这个套接字只负责监听,不负责和客户端收发数据
  2. struct sockaddr *addr::输出型参数,用来存放客户端的 IP 地址和端口号
    特点
    这是一个传出参数,函数执行成功后,里面会自动填入客户端信息
    如果你不关心客户端是谁,可以直接传 NULL
    常用类型:实际使用时一般传 struct sockaddr_in 类型,然后强转
  3. socklen_t *addrlen:地址结构体长度的指针
    作用
    传入:告诉内核你的地址结构体有多大;传出:内核返回实际写入的地址结构体长度
    特点
    必须是指针类型,如果上面 addr 传了 NULL,这里也必须传 NULL
  4. 返回值 :成功返回一个全新的套接字文件描述符 (connfd)这个新 fd 专门用来和客户端通信 (read / write)原来的 sockfd 继续监听其他客户端
cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  1. 作用:服务器先用 socket、bind、listen 打开一个端口,开始等着客户端来连,客户端调用 connect 连上来,三次握手完成后,连接会在内核里排队,你调用 accept():内核从队列里拿一个连接出来给你返回一个新的文件描述符以后和这个客户端发数据、收数据,都用这个新 fd
  2. 注意原来的 sockfd(监听 fd)只负责等新连接,accept 返回的新 fd 只负责和单个客户端通信,如果队列里没有连接,accept 默认会阻塞等待,直到有人连接
  3. int sockfd服务器的监听套接字 ,socket() → bind() → listen() 那一套用的那个 fd,它只负责监听,不负责收发数据
  4. struct sockaddr *addr:它是一个输出型参数,用来存放客户端的ip和端口,
  5. socklen_t *addrlen:表示struct sockaddr *addr的长度
  6. 返回值 (重要):成功返回一个新的文件描述符(专门用来和这个客户端通信),失败返回 -1

二、多进程版本

这个要处理的业务是:完成一个英译汉的翻译功能

代码部分:

要想实现这个其实也很简单的,来看核心代码

cpp 复制代码
			pid_t id = fork();
            if (id < 0)
            {
                std::cout << "fork error" << std::endl;
                exit(FORK_ERR);
            }
            else if (id == 0) // 子进程
            {
                // 这里子进程不光可以能看见_sockfd,还能看见_listenscokfd,但是这里不想让子进程看见listensockfd,所以要关闭它
                close(_listensockfd); // 关闭不需要的文件描述符

                if (fork() > 0)
                    exit(OK); // 再次fork让子进程退出,让孙子进程来提供服务。这里这样写是防止父进程等待的时候阻塞,
                Service(_sockfd, addr);
                exit(OK);
            }
            else // 父进程
            {
                // 父进程也要关闭不需要的文件描述符
                close(_sockfd);
                pid_t pid = waitpid(id, nullptr, 0); // 这里在等待的时候是阻塞的,可以用signal(SIGCHLD, SIG_IGN); 忽略SIG_IGN信号,推荐的做法
                (void)pid;
            }			

这里我们是用子进程的子进程(孙子进程)来提供服务的,让子进程退出,不然父进程会一直等待子进程,导致服务端阻塞,然后孙子进程提供完服务后再退出时会被会被 init 进程(systemd)领养也就是 PID = 1 的那个进程。这样几解决了有避免了僵尸进程。还有个方法就是用子进程来提供服务,只不过这样把信号给忽略了,不然就会阻塞,用signal(SIGCHLD, SIG_IGN); ,这两中方法都是可以的。

代码的话就上面的第一个差不多一样的,只是这个要实现音译汉的功能,所以只需要左稍微改动就行了。来看具体代码,一共需要这些文件:

TcpServer.cc(服务端),TcpClient.cc(客户端),TcpServer.hpp(服务端的开源文件),Common.hpp(禁止拷贝和错误码设置文件),InetAddr.hpp(端口和ip的转换文件),Makefile(自动编译文件),Dict.hpp(处理数据和翻译),dictionary.txt(数据库)

1.TcpServer.cc(服务端)

cpp 复制代码
#include <iostream>
#include "TcpServer.hpp"
#include <memory>
#include "Dict.hpp"

void Usage(std::string line)
{
    std::cout << line << " port " << std::endl;
}
//./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = std::stoi(argv[1]);

    Dict d;
    d.LoadDict();

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&d](const std::string &word, InetAddr &addr)
                                                                  { return d.Translate(word, addr); });

    tsvr->Init();
    tsvr->Run();
}

2.TcpClient.cc(客户端)

cpp 复制代码
#include"Common.hpp"
#include<iostream>
#include"InetAddr.hpp"
//./tcpclient ip port

//判断是否全是空白字符(空格、Tab、换行)
bool AllSpace(const std::string& s)
{
    for (auto c : s)
    {
        if (!isspace(c))return false;
    }
    return true;
}
//提示正确用法            
void Usage(std::string proc)
{
    std::cout<<proc<<" ip  port"<<std::endl;
}

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string _ip=argv[1];
    uint16_t _port=std::stoi(argv[2]);

    //创建套接字
    int _sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(_sockfd<0)
    {
        std::cout<<"_sockfd error"<<std::endl;
        exit(SOCK_ERR);
    }

    //这里也需要绑定,只不过不需要显示绑定,随机方式选择端口号,listen,accept都不需要做,那是服务器的事

    //向目标服务器直接发起建立连接的请求
    InetAddr client(_ip,_port);
    int n=connect(_sockfd,client.NetAddrPtr(),client.InetAddrLen());
    if(n<0)
    {
        std::cout<<"connet error"<<std::endl;
        exit(CONNECT_ERR);
    }

    while(true)
    {
        //发消息
        std::string line;
        std::cout<<"Please Enter#";
        std::getline(std::cin,line);

        //空白字符(空格、Tab、换行)不发送
        if(line.empty()||AllSpace(line))continue;
        
        write(_sockfd,line.c_str(),line.size());

        //收消息
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer)); // 清空缓冲区,避免乱码
        ssize_t s= read(_sockfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            std::cout<<buffer<<std::endl;
        }
        else if(s==0)
        {
           break;
        }
        else break;
    }
    close(_sockfd);
    return 0;
}

3.TcpServer.hpp(服务端的开源文件)

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/wait.h>
#include <unistd.h>
#include "Common.hpp"
#include "InetAddr.hpp"

const static int defaultsockfd = -1;
const static int backlog = 253;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;

class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func) : _port(port), _listensockfd(defaultsockfd), isrunning(false), _func(func)
    {
    }
    void Init()
    {
        // 创建监听套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            std::cout << "socket error" << std::endl;
            exit(SOCK_ERR);
        }
        std::cout << "sockfd success:" << _listensockfd << std::endl;

        // 绑定端口和ip
        InetAddr peer(_port);
        int n = bind(_listensockfd, peer.NetAddrPtr(), peer.InetAddrLen());
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success:" << n << std::endl;

        // 监听套接字
        int l = listen(_listensockfd, backlog);
        if (l < 0)
        {
            std::cout << "listen error" << std::endl;
            exit(LISTEN_ERR);
        }
        std::cout << "listen success:" << l << std::endl;
    }
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 读取客服端的消息
            ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);//这里后bug,后面在解释
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << peer.StringAddr() << "say:" << buffer << std::endl;

                // 回显给客户端的消息
                std::string echo = _func(buffer, peer);
                std::string line = "server echo : ";
                line += echo;
                write(sockfd, line.c_str(), line.size());
            }
            if (s <= 0)
            {
                std::cout << "客户端退出" << std::endl;
                break; // 处理客户端退出情况
            }
        }
        close(sockfd); // 客户端退出,关闭文件描述符,释放资源
    }
    void Run()
    {
        isrunning = true;
        while (isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 获取连接并返回一个文件描述符来提供服务,如果没有远端来连接的话,它会一直阻塞在这里。
            int _sockfd = accept(_listensockfd, CONV(peer), &len);
            if (_sockfd < 0)
            {
                std::cout << "accept cerr" << std::endl;
                continue;
            }

            InetAddr addr(peer);
            std::cout << "accept success:" << addr.StringAddr() << std::endl;

            pid_t id = fork();
            if (id < 0)
            {
                std::cout << "fork error" << std::endl;
                exit(FORK_ERR);
            }
            else if (id == 0) // 子进程
            {
                // 这里子进程不光可以能看见_sockfd,还能看见_listenscokfd,但是这里不想让子进程看见listensockfd,所以要关闭它
                close(_listensockfd); // 关闭不需要的文件描述符

                if (fork() > 0)
                    exit(OK); // 再次fork让子进程退出,让孙子进程来提供服务。这里这样写是防止父进程等待的时候阻塞,避免僵尸进程出现。
                Service(_sockfd, addr);
                exit(OK);
            }
            else // 父进程
            {
                // 父进程也要关闭不需要的文件描述符
                close(_sockfd);
                pid_t pid = waitpid(id, nullptr, 0); // ,这里在等待的时候是阻塞的,可以用signal(SIGCHLD, SIG_IGN); 忽略SIG_IGN信号,推荐的做法
                (void)pid;
            }
        }
        isrunning = false;
    }
    ~TcpServer() // 关闭监听套接字
    {
        if (_listensockfd != defaultsockfd)
        {
            close(_listensockfd);
            _listensockfd = defaultsockfd;
        }
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool isrunning;
    func_t _func;
};

4.Common.hpp(禁止拷贝和错误码设置文件)

cpp 复制代码
#pragma once
#include<iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum  ExitCode
{
    OK=0,
    USAGE_ERR,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    WRITE_ERR,
    FORK_ERR
};

class NoCopy
{
public:
    NoCopy(){}
    NoCopy(const NoCopy&)=delete;
    const NoCopy&operator=(const NoCopy&)=delete;
    ~NoCopy(){}

private:
};

#define  CONV(addr) ((struct sockaddr*)&addr)

5.InetAddr.hpp(端口和ip的转换文件)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Common.hpp"
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &addr) // 网络格式转换成本地格式
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr); // 四字节网络ip风格转换成点分十进制
    }

    InetAddr(uint16_t port)
        : _port(port)
    {

        bzero(&_addr, sizeof(_addr)); // 清空sockaddr_in
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port); // 本地格式转化成网络格式
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(std::string ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        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());
    }
    std::string StringAddr() const
    {
        return _ip + ":" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }

    // 返回port和ip
    uint16_t port() { return _port; }
    std::string ip() { return _ip; }

    const struct sockaddr_in &NetAddr() const { return _addr; }

    const struct sockaddr *NetAddrPtr() { return CONV(_addr); }

    socklen_t InetAddrLen() { return sizeof(_addr); }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    uint16_t _port;
    std::string _ip;
};

6.Dict.hpp(处理数据和翻译)

cpp 复制代码
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "InetAddr.hpp"
const std::string defaultdict="./dictionary.txt";
const std::string sep=": ";
class Dict
{
public:
    Dict(const std::string &path = defaultdict)
    :_dict_path(path)
    {}

    bool LoadDict()//主要实现功能就是把数据从文件中提取出来放在哈希容器中
    {
        std::ifstream in(_dict_path);//打开文件
        if(!in.is_open())
        {
            std::cout<<"打开文件错误"<<std::endl;
            return false;
        }
        std::string line;
        while(std::getline(in,line))//一行一行读取文件放在line中
        {
            auto pos=line.find(sep);
            if(pos==std::string::npos)
            {
                std::cout<<"加载失败"<<line<<std::endl;
                continue;
            }
            std::string english=line.substr(0,pos);
            std::string chinese=line.substr(pos+sep.size());

            if(english.empty()||chinese.empty())
            {
                std::cout<<"没有有效内容"<<std::endl;
                continue;
            }

            _dict.insert(std::make_pair(english, chinese));
        }
        in.close();
        return true; 
    }
    std::string Translate(const std::string &word, InetAddr &client)//主要实现功能就是从哈希表中查找是否有该数据。
    {
        auto it=_dict.find(word);
        if(it==_dict.end())
        {
            std::cout<<"进入翻译模式:"<<"["<<client.ip()<<"]#"<<"None"<<std::endl;
            return "None";
        }
        std::cout<<"进入翻译模式:"<<"["<<client.ip()<<"]#"<<word<<std::endl;
        return it->second;
       
    }
    
    ~Dict(){}
private:
    std::string _dict_path;
    std::unordered_map<std::string ,std::string> _dict; //把截取的字符串放在一个哈希容器中

};

7.dictionary.txt(数据库)

数据空白和缺失部分是这里为了方便测试,如果不需要也可以自己找一份数据。

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好



run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

8.Makefile(自动编译文件)

bash 复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17 
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f tcpclient tcpserver 

三、多线程版本

这一部分主要实现一个输入指令,让服务端给我把指令的结果回显到客户端。我也对代码进行了相应的完善了补充。先来看实现效果:

代码部分

想要实现上面的代码一共需要这些文件:

TcpServer.cc(服务端),TcpClient.cc(客户端),TcpServer.hpp(服务端的开源文件),InetAddr.hpp(端口号和ip的转换),Common.hpp(错误码和防拷贝功能),Command.hpp(用来执行命令并返回结果),Makefile(自动编译文件)

1.TcpServer.cc(服务端)

cpp 复制代码
#include <iostream>
#include "TcpServer.hpp"
#include <memory>
#include "Command.hpp"

void Usage(std::string line)
{
    std::cout << line << " port " << std::endl;
}
//./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = std::stoi(argv[1]);

    Command cmd;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));
    // 这个跟上面的是一样的。
    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&cmd](const std::string &commd, InetAddr &client)
    //                                                             { return cmd.Execute(commd, client); });
    tsvr->Init();
    tsvr->Run();
}

2.TcpClient.cc(客户端)

cpp 复制代码
#include"Common.hpp"
#include<iostream>
#include"InetAddr.hpp"
//./tcpclient ip port

//判断是否全是空白字符(空格、Tab、换行)
bool AllSpace(const std::string& s)
{
    for (auto c : s)
    {
        if (!isspace(c))return false;
    }
    return true;
}
//提示正确用法            
void Usage(std::string proc)
{
    std::cout<<proc<<" ip  port"<<std::endl;
}

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string _ip=argv[1];
    uint16_t _port=std::stoi(argv[2]);

    //创建套接字
    int _sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(_sockfd<0)
    {
        std::cout<<"_sockfd error"<<std::endl;
        exit(SOCK_ERR);
    }

    //这里也需要绑定,只不过不需要显示绑定,随机方式选择端口号,listen,accept都不需要做,那是服务器的事

    //向目标服务器直接发起建立连接的请求
    InetAddr client(_ip,_port);
    int n=connect(_sockfd,client.NetAddrPtr(),client.InetAddrLen());
    if(n<0)
    {
        std::cout<<"connet error"<<std::endl;
        exit(CONNECT_ERR);
    }

    while(true)
    {
        //发消息
        std::string line;
        std::cout<<"Please Enter#";
        std::getline(std::cin,line);

        //空白字符(空格、Tab、换行)不发送
        if(line.empty()||AllSpace(line))continue;
        
        write(_sockfd,line.c_str(),line.size());

        //收消息
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer)); // 清空缓冲区,避免乱码
        ssize_t s= read(_sockfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            std::cout<<buffer<<std::endl;
        }
        else if(s==0)
        {
           break;
        }
        else break;
    }
    close(_sockfd);
    return 0;
}

3.TcpServer.hpp(服务端的开源文件)

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>   // strerror
#include <pthread.h> // 线程
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Command.hpp"
const static int defaultsockfd = -1;
const static int backlog = 253;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;

class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func) : _port(port), _listensockfd(defaultsockfd), isrunning(false), _func(func)
    {
    }
    void Init()
    {
        // 创建监听套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            std::cout << "socket error" << std::endl;
            exit(SOCK_ERR);
        }
        std::cout << "sockfd success:" << _listensockfd << std::endl;

        // 绑定端口和ip
        InetAddr peer(_port);

        int n = bind(_listensockfd, peer.NetAddrPtr(), peer.InetAddrLen());
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success:" << n << std::endl;

        // 监听套接字
        int l = listen(_listensockfd, backlog);
        if (l < 0)
        {
            std::cout << "listen error" << std::endl;
            exit(LISTEN_ERR);
        }
        std::cout << "listen success:" << l << std::endl;
    }

    class ThreadData // 类部类
    {
    public:
        ThreadData(int sock, InetAddr &ar, TcpServer *ptr)
            : sockfd(sock), addr(ar), tsvr(ptr)
        {
        }

    public:
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;

    private:
    };
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true) // read 和write都是存在bug的,就是粘包问题,后面会解释的。不过这个为了测试,就先这样写了
        {
            // 读取客服端的消息
            ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
            if (s > 0)
            {

                buffer[s] = 0; // 收到的命令命令
                std::string result;
                if (_func) // 判断是否是空函数,防止代码崩溃
                {
                    result = _func(buffer, peer); // 回调方法返回命令结果
                }
                else
                {
                    result = "服务器未注册处理函数\n";
                }

                ssize_t ret = write(sockfd, result.c_str(), result.size()); // 在写回给客户端
                if (ret < 0)
                {
                    std::cout << peer.StringAddr() << " 客户端已断开,写入失败" << std::endl;
                    break;
                }
            }
            else if (s == 0)
            {
                std::cout << peer.StringAddr() << " 客户端正常退出退出" << std::endl;
                break; // 处理客户端退出情况
            }
            else
            {
                std::cout << peer.StringAddr() << " 读取失败 " << std::endl;
                break; // 处理客户端退出情况
            }
        }
        close(sockfd); // 客户端退出,关闭文件描述符,释放资源
    }
    static void *Routine(void *args) // 执行方法
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }
    void Run()
    {
        isrunning = true;
        while (isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 获取连接并返回一个文件描述符来提供服务,如果没有远端来连接的话,它会一直阻塞在这里。
            int _sockfd = accept(_listensockfd, CONV(peer), &len);
            if (_sockfd < 0)
            {
                std::cout << "accept cerr" << std::endl;
                continue; // 获取连接不成功就重新获取
            }
            InetAddr addr(peer);
            std::cout << "accept success:" << addr.StringAddr() << std::endl;

            // version2: 多线程版本
            ThreadData *td = new ThreadData(_sockfd, addr, this);
            pthread_t tid;
            int ret = pthread_create(&tid, nullptr, Routine, td);
            if (ret != 0)
            {
                std::cerr << "线程创建失败:" << strerror(ret) << std::endl;
                // 关闭相应的描述符
                close(_sockfd); // 关闭套接字
                delete td;      // 释放内存
                continue;
            }
        }
        isrunning = false;
    }
    ~TcpServer() // 关闭监听套接字
    {
        if (_listensockfd != defaultsockfd)
        {
            close(_listensockfd);
            _listensockfd = defaultsockfd;
        }
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool isrunning;
    func_t _func;
};

4.InetAddr.hpp(端口号和ip的转换)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Common.hpp"
class InetAddr
{
public:
    InetAddr() {};
    InetAddr(const struct sockaddr_in &addr) // 网络格式转换成本地格式
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr); // 四字节网络ip风格转换成点分十进制
    }

    InetAddr(uint16_t port)
        : _port(port)
    {

        bzero(&_addr, sizeof(_addr)); // 清空sockaddr_in
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port); // 本地格式转化成网络格式
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(std::string ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        int res = inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        if (res <= 0)
        {
            std::cerr << "IP 地址无效: " << _ip << std::endl;
        }
    }
    std::string StringAddr() const
    {
        return _ip + ":" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }

    // 返回port和ip
    uint16_t port() { return _port; }
    std::string ip() { return _ip; }

    const struct sockaddr_in &NetAddr() const { return _addr; }

    const struct sockaddr *NetAddrPtr() { return CONV(_addr); }

    socklen_t InetAddrLen() { return sizeof(_addr); }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    uint16_t _port;
    std::string _ip;
};

5.Common.hpp(错误码和防拷贝功能)

cpp 复制代码
#pragma once
#include<iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum  ExitCode
{
    OK=0,
    USAGE_ERR,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    WRITE_ERR,
    FORK_ERR
};

class NoCopy
{
public:
    NoCopy(){}
    NoCopy(const NoCopy&)=delete;
    const NoCopy&operator=(const NoCopy&)=delete;
    ~NoCopy(){}

private:
};

#define  CONV(addr) ((struct sockaddr*)&addr)

6.Command.hpp(用来执行命令并返回结果)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Command.hpp"
#include "InetAddr.hpp"
// using namespace LogModule;

class Command
{
public:
    Command()
    { // 添加shell命令,只有在这里面的才能被执行
        _whiteListCommands.insert("ls");
        _whiteListCommands.insert("pwd");
        _whiteListCommands.insert("ls -l");
        _whiteListCommands.insert("touch test.cc");
        _whiteListCommands.insert("whoami");
    }
    // 判断命令是否在set里面
    bool isSeafeCommand(const std::string &cmd)
    {
        auto iter = _whiteListCommands.find(cmd);
        return iter != _whiteListCommands.end();
    }
    std::string Execute(const std::string &cmd, InetAddr &perr)
    {
        // 检验命令是否可执行(在白名单中的可执行)
        if (!isSeafeCommand(cmd))
        {
            return std::string("不能执行");
        }

        // 执行 命令
        FILE *fd = popen(cmd.c_str(), "r"); // popen是 Unix/Linux 下的标准 C 库函数,核心作用是创建一个管道,fork 出子进程执行 shell 命令,
                                            // 并返回一个可读写的 FILE 指针,让父进程能像操作文件一样与子进程通信。
        if (fd == nullptr)
        {
            return std::string("你要执行的命令不存在") + cmd;
        }

        std::string hwo = perr.StringAddr();
        std::string result;
        char line[1024];
        while (fgets(line, sizeof(line), fd)) // fget是从管道中一行一行读取数据
        {
            result += line;
        }

        pclose(fd); // 关闭文件描述符

        return hwo + " Execute done,result is :\n" + result; // 返回结果
    }
    ~Command() {}

private:
    std::set<std::string> _whiteListCommands; // 白名单
};

7.Makefile(自动编译文件)

bash 复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17 
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17 -lpthread

.PHONY:clean
clean:
	rm -f tcpclient tcpserver 

总结

以上就是本章要实现的功能,由于代码部分比较多,解释的部分很少,所以有什么问题欢迎讨论。

相关推荐
头铁的伦5 小时前
QNX 网络模型
linux·网络·车载系统
Q3_SkyAsh5 小时前
【电子取证】——第三届“平航杯”电子数据取证竞赛服务器取证部分
服务器·电子取证
vortex55 小时前
构建可审计、可分层、可扩展的SSH身份管理体系
网络·ssh·php
大白菜和MySQL5 小时前
apache服务器部署简记
运维·服务器·apache
渣渣馬6 小时前
rk3588s的firfly的linux的sdk版本
linux·运维·服务器
Hello_Embed6 小时前
嵌入式上位机开发入门(十九):Socket 状态检测与断线重连
网络·单片机·网络协议·tcp/ip·嵌入式
哎嗨人生公众号6 小时前
手写求导公式,让轨迹优化性能飞升,150ms变成9ms
开发语言·c++·算法·机器人·自动驾驶
code_whiter6 小时前
C++6(模板)
开发语言·c++
一只旭宝6 小时前
【C++ 入门精讲1】初始化、const、引用、内联函数 | 超详细手写笔记(附完整代码)
开发语言·c++