Linux上一个简单的echo服务器搭建

一.简介

文章将讲解简单的echo服务器(阻塞)搭建的过程,介绍各函数的用法和定义。

二.头文件结构体定义和源文件函数实现

1.TcpStream.h

结构体定义

cpp 复制代码
#pragma once
#include<string>
#include<cstdint>
using namespace std;

class TcpStream 
{
    public:
    explicit TcpStream(int sock=-1);    //显式构造,防止隐式转换
    ~TcpStream();       //析构函数,自动关闭socket

    bool send_message(const string msg);    //发送一条完整消息(长度头+消息体)
    string recv_message();      //接受一条完整消息,失败返回空串

    bool is_valid() const { return sock_!=1; }        //判断对象是否有效

    //手动关闭
    void close();

    private:
    int sock_;      //socket文件描述符
    bool send_all(int sock,const char* buf,size_t len);      //发送全部消息
    bool receive_all(int sock,char* buf,size_t len);   //接收全部消息
};

is_valid()函数用于判断创建的socket或者接收连接后的socket是否有效。

函数实现

cpp 复制代码
#include"TcpStream.h"
#include<sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>
#include<arpa/inet.h>
using namespace std;

//发送消息静态辅助函数
bool TcpStream::send_all(int sock,const char *buf,size_t len)
{
    size_t sent=0;      //记录已发送的数据字节数
    while(sent<len)
    {
        ssize_t n=send(sock,buf+sent,len-sent,0);
        if(n==-1)       //发送错误
        {
            return false;
        }
        sent +=n;
    }
    return true;
}

//接收消息静态辅助函数
bool TcpStream::receive_all(int sock,char *buf,size_t len)
{
    size_t received=0;      //记录已接收的数据字节数
    while(received<len)
    {
        ssize_t n=recv(sock,buf+received,len-received,0);
        if(n<=0)
        {
            if(n==-1) cout<<"receive error"<<endl;
            if(n==0) cout<<"对端关闭连接"<<endl;
            return false;
        }
        received +=n;
    }
    return true;
}

//构造函数
TcpStream::TcpStream(int sock):sock_(sock){};

//析构函数
TcpStream::~TcpStream(){
    close();
}

//发送消息
bool TcpStream::send_message(const string msg)
{
    if(!is_valid()) return false;
    uint32_t net_len=htonl(static_cast<uint32_t>(msg.size()));
    if(!send_all(sock_,reinterpret_cast<char*>(&net_len),4)){
        return false;
    }
    if(!send_all(sock_,msg.c_str(),msg.size())){
        return false;
    }
    return true;
}
 
//接收消息
string TcpStream::recv_message()
{
    if(!is_valid()) return "";
    uint32_t net_len;
    if(!receive_all(sock_,reinterpret_cast<char*>(&net_len),4)){
        return "";
    }
    uint32_t len=ntohl(net_len);
    if(len>1024*1024) return "";
    string buf(len,'\0');
    if(!receive_all(sock_,&buf[0],len)){
        return "";
    }
    return buf;
}

void TcpStream::close()
{
    if(sock_!=-1) 
    {::close(sock_);
    sock_=-1;}
}
  • bool send_all(int sock,const* buf,size_t len)和bool receive_all(int sock,const* buf,size_t len)函数是辅助发送和接收函数,函数中会分别用到send和recv函数。
  • 函数bool send_message(con string msg)用于发送一条完整的数据,包含长度头和消息体,其中长度头用于标志消息体的长度和防止粘包。客户端发送数据和服务端发送反馈到客户端用到此函数。此函数实现会调用send_all和receive_all函数。
    返回值:成功返回true失败返回false。
  • 函数string recv_message()用于接收一条完整的数据,同样包含长度头和消息体。客户端接收服务端反馈和服务端接收客户端的消息用到此函数。
    返回值:成功返回接收到的消息,失败返回空string字符串。
  • close()函数用于关闭socket接口。
2.TcpServer.h
cpp 复制代码
#pragma once
#include "TcpStream.h"
#include<cstdint>

class TcpServer{
    public:
    //构造函数:监听指定窗口
    TcpServer(uint16_t port,int backlog=10);
    //析构函数:关闭监听
    ~TcpServer();

    //检查服务器是否成功启动
    bool is_valid() const {return listen_fd !=-1;}

    //接受一个新连接,返回新的socket文件描述符,失败返回-1
    int accept_();

    // 手动关闭监听 socket
    void close();

    private:
    int listen_fd;
};

函数实现

cpp 复制代码
#include "TcpServer.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>
using namespace std;

//构造函数
TcpServer::TcpServer(uint16_t port,int backlog):listen_fd(-1)       //backlog指定队列最大长度
{
    //创建socket
    listen_fd=::socket(AF_INET,SOCK_STREAM,0);
    if(listen_fd==-1)
    {
        cerr<<"socket error"<<strerror(errno)<<endl;
        return;
    }
    //设置端口重用
    int opt=1;
    if(::setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))==-1)
    {
        cerr<<"setsockopt() error"<<strerror(errno)<<endl;
        close();
        return;
    }

    //绑定地址
    sockaddr_in addr{};
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_ANY);    //监听所有端口
    addr.sin_port=htons(port);
    if(::bind(listen_fd,(sockaddr*)&addr,sizeof(addr))==-1)
    {
        cerr<<"bind() error"<<strerror(errno)<<endl;
        close();
        return;
    }

    //开始监听
    if(::listen(listen_fd,backlog)==-1)
    {
        cerr<<"listen() error"<<strerror(errno)<<endl;
        close();
        return;
    }
    cout<<"Server listening on port:"<<port<<endl;
}

TcpServer::~TcpServer() {
    close();
}

int TcpServer::accept_()
{
    if(!is_valid()) return -1;

    sockaddr_in client_addr;
    socklen_t len=sizeof(client_addr);
    int client_fd=::accept(listen_fd,(sockaddr*)&client_addr,&len);
    if (client_fd == -1) {
        std::cerr << "accept_() error: " << strerror(errno) << std::endl;
        return -1;
    }

    //显示客户端ip和端口(用于调试)
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET,&client_addr.sin_addr,ip,sizeof(ip));
    cout<<"来自"<<ip<<" : "<<ntohl(client_addr.sin_port)<<"的新连接"<<endl;
    return client_fd;
}

void TcpServer::close() {
    if (listen_fd != -1) {
        ::close(listen_fd);
        listen_fd = -1;
    }
}
  • 初始化构造函数TcpServer时创建一个监听接口,然后设置端口重用(为了避免服务器关闭后立即重启时因 TIME_WAIT 状态导致绑定失败),然后绑定ip地址和端口,开始监听。
  • accept_函数负责接收请求连接的socket,然后创建新的socket即client_fd,用于后续数据的发送和接收,然后输出已连接的客户端的ip地址和端口号。

三.服务端和客户端的main函数

1.服务端
cpp 复制代码
#include "TcpServer.h" 
#include "TcpStream.h"
#include<iostream>
using namespace std;
int main()
{
    uint16_t port;
    cout<<"请输入要监听的端口(目前只有8888):";
    cin>>port;
    TcpServer server(port);
    if(!server.is_valid())
    {
        cerr<<"服务端启动失败"<<endl;
        return 1;
    }
    cout<<"Server listening on port:"<<port<<endl;
    while(true)
    {
        int client_fd=server.accept_();
        if(client_fd==-1) continue; //出错继续下一个循环
        TcpStream client(client_fd);
        if(!client.is_valid()) continue;
        bool client_error =false;
        while(!client_error)
        {
            string msg=client.recv_message();
            if(msg.empty()){
                client_error=true;
                break;
            }
            string response="服务端接收到:"+msg;
            cout<<response<<endl;
            if(!client.send_message(response)) {
                cout<<"发送反馈失败"<<endl;
                client_error=true;
                break;
            }
        }
    }
    return 0;
}
  • 输入端口号后初始化TcpServer对象server,初始化时完成了监听接口listen_fd的创建,端口重用的设置,然后绑定ip和端口,最后开始监听。
  • 监听到请求连接的客户端后进入while循环,创建新接口client_fd接收连接,然后用client_fd初始化TcpStream对象,用client_error标志客户端是否出错,进入第二个while循环,开始接收客户端发送的消息,由于是阻塞状态,所以一次只能单线程处理一个客户端。接收消息完毕(完整的消息)后向客户端发送反馈。
2.客户端
cpp 复制代码
#include "TcpStream.h"
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main() {
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock==-1)
    {
        cerr<<"创建socket出错"<<endl;
        return 1;
    }

    //配置ip和端口
    sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(8888);
    if(inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr)<=0)
    {
        cerr<<"不合法的地址"<<endl;
        close(sock);
        return 1;
    }
    if(connect(sock,(sockaddr*)&addr,sizeof(addr))==-1)
    {
        cerr<<"连接失败"<<endl;
        close(sock);
        return 1;
    }
    TcpStream stream(sock);
    cout<<"已连接到服务端"<<endl;
    string input;
    while(true)
    {
        cout<<"请输入要发送的消息"<<endl;
        getline(cin,input);
        if(input=="exit")
        {
            cout<<"已关闭"<<endl;
            close(sock);
            break;
        }
        if(!stream.send_message(input))
        {
            cerr<<"发送失败,请重试"<<endl;
            continue;
        }
        string reply=stream.recv_message();
        if(reply.empty())
        {
            cerr<<"接收反馈失败"<<endl;
            continue;
        }
        cout<<"服务端反馈:"<<reply<<endl;
    }
    return 0;
}
  • 先创建一个客户端的sokcet,随后绑定ip和端口,然后用connect函数连接服务端。
  • 然后用深刻的的接口初始化TcpSTream对象,进入while循环,输入要发送的消息回车发送(单独输入exit断开连接),然后接收服务端的反馈。

注意:要先启动服务端再启动客户端。

相关推荐
努力努力再努力wz1 小时前
【MySQL入门系列】:不只是建表:MySQL 表约束与 DDL 执行机制全解析
android·linux·服务器·数据结构·数据库·c++·mysql
前端技术1 小时前
负载均衡组件 -loadBalancer 无法获取服务端信息问题
运维·负载均衡
bukeyiwanshui2 小时前
20260416 DHCP以及DNS
linux·网络
zhojiew2 小时前
在中国区aws通过Network Flow Monitor实现实例网络流量指标上传到cloudwatch
服务器·网络·aws
ALex_zry2 小时前
Converter双向转换的边界条件处理
运维·服务器·建造者模式
IMPYLH2 小时前
Linux 的 printf 命令
linux·运维·服务器·bash
国信DRS杭州数据恢复2 小时前
浪潮服务器RAID5磁盘阵列VMFS文件系统下虚拟机误删除数据恢复
运维·科技·硬件架构·硬件工程·运维开发
Coco_淳2 小时前
linux 服务器 初始化数据盘
运维·服务器