Linux--Socket 编程 TCP(Echo Server)

目录

1.认识TCP接口

  2.Echo Server

2.1添加的日志系统(代码)

 2.2解析网络地址

 2.3 服务端逻辑 (代码)

2.4客户端逻辑(代码) 

2.5代码测试 


1.认识TCP接口

下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。
socket():

• socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描
述符;
• 应用程序可以像读写文件一样用 read/write 在网络上收发数据;
• 如果 socket()调用出错则返回-1;
• 对于 IPv4, family 参数指定为 AF_INET;
• 对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
• protocol 参数的介绍从略,指定为 0 即可。

bind():

• 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服
务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一
个固定的网络地址和端口号;
• bind()成功返回 0,失败返回-1。
• bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络
通讯的文件描述符监听 myaddr 所描述的地址和端口号;
• 前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受
多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen
指定结构体的长度;
 

我们的程序中对 myaddr 参数是这样初始化的:

1. 将整个结构体清零;
2. 设置地址类型为 AF_INET;
3. 网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有
多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听,
直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址;
4. 端口号为 SERV_PORT, 我们定义为 9999;

listen():

• listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接
等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5)。
• listen()成功返回 0,失败返回-1;
 

accept():

• 三次握手完成后, 服务器调用 accept()接受连接;
• 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端
连接上来;
• addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
• 如果给 addr 参数传 NULL,表示不关心客户端的地址;
• addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提
供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实
际长度(有可能没有占满调用者提供的缓冲区);

• 返回值:

  • 如果accept()成功,它将返回一个新的套接字描述符,这个描述符用于与客户端的通信。原始的sockfd套接字描述符则继续留在监听状态,准备接受新的连接请求。

  • 如果accept()失败,它将返回-1,并设置全局变量errno以指示错误的类型。

我们的服务器程序结构是这样的:

connect

• 客户端需要调用 connect()连接服务器;
• connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而
connect 的参数是对方的地址;
• connect()成功返回 0,出错返回-1;
 


  2.Echo Server


2.1添加的日志系统(代码)

 LockGuard.hpp

#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t *_mutex;
};

Log.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }
        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._id,
                   lg._filename.c_str(),
                   lg._filenumber,
                   lg._curr_time.c_str(),
                   lg._message_info.c_str());
        }
        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._id,
                     lg._filename.c_str(),
                     lg._filenumber,
                     lg._curr_time.c_str(),
                     lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }
        void FlushLog(const logmessage &lg)
        {
            // 加过滤逻辑 --- TODO

            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出来日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define LOG(Level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
#define EnableFILE()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};


 2.2解析网络地址

 此功能实现较为简单,请看注释:

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//网络地址
class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)//网络序列转主机序列
    {
        _port = ntohs(addr.sin_port);//这个端口号是随机bind的
        _ip = inet_ntoa(addr.sin_addr);//四字节地址转字符串
    }

public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;//保存一下网络序列
};


 2.3 服务端逻辑 (代码)

TcpServer.hpp代码主体逻辑:

  1. 构造函数 (TcpServer(uint16_t port = gport)): 初始化服务器对象时,可以指定一个端口号(默认为8888)。它设置了服务器的监听端口、监听套接字(初始化为-1,实际值在InitServer中设置)和服务器是否正在运行的标志。

  2. 初始化服务器 (InitServer()): 这个方法负责设置服务器的网络环境。它首先创建一个socket,然后将其绑定到服务器的地址和端口上,并监听连接请求。如果在这个过程中发生任何错误(如socket创建失败、绑定失败或监听失败),则会记录错误日志并退出程序。

  3. 启动服务器循环 (Loop()): 这个方法是服务器的主循环,它首先设置_isrunningtrue,然后进入一个无限循环,不断接受客户端的连接请求。对于每个新的连接请求,它都会调用accept函数来获取一个新的socket(sockfd),该socket用于与客户端进行通信。然后,它调用Service函数来处理这个连接。如果_isrunning被设置为false(尽管在这个示例中没有显示直接设置它的代码,但在实际应用中可能需要某种方式来优雅地关闭服务器),则循环会终止。

  4. 处理连接 (Service(int sockfd, InetAddr addr)): 这个方法负责处理与客户端的通信。它进入一个无限循环,不断从客户端socket读取数据,并将读取到的数据(前面添加了"[server echo] #"前缀)发送回客户端。如果读取到0字节(表示客户端关闭了连接),或者发生读取错误,循环会终止,并且会关闭与客户端的socket连接。

#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERR
};

const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;//套接字监听队列的最大长度


class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
        : _port(port),
          _listensockfd(gsock),
          _isrunning(false)
    {
    } 
    void InitServer()//初始化
    {
        // 1. 创建socket
        //SOCK_STREAM表示流式套接字(TCP典型),AF_INET代表IPv4地址族
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "socket create error\n");
            exit(SOCKET_ERROR);
        }
        //创建socket成功
        LOG(INFO, "socket create success, sockfd: %d\n", _listensockfd); // 3

  // 2. bind
        struct sockaddr_in local;//用于表示Internet地址,特别是IPv4地址和端口号。
        memset(&local, 0, sizeof(local));//先清空再使用
        local.sin_family = AF_INET;//表示你的套接字将使用IPv4协议。
        local.sin_port = htons(_port);//端口号,htons主机序列转为网络序列
        // local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定

        // 2. bind sockfd 和 Socket addr
        if (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");

        // 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
        //将套接字设置为监听进入连接的状态,准备接受客户端的连接请求
        if (::listen(_listensockfd, gblcklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERR);
        }
        LOG(INFO, "listen success\n");
    }

    void Loop()//启动
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 4. 获取新连接
            //返回的为IO套接字,该过程有用户连接才通过_listensockfd获取新链接,否则阻塞等待
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;//再继续accept
            }
            InetAddr addr(client);
            //来了一个新链接
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);
            Service(sockfd, addr);//提供服务
        }
        _isrunning = false;
    }
    void Service(int sockfd, InetAddr addr)
    {
        // 长服务(客户不停止,服务器也不停止)
        while (true)
        {
            char inbuffer[1024]; // 当做字符串
            //像读文件一样从sockfd中读(接收消息)
            ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);

                std::string echo_string = "[server echo] #";
                echo_string += inbuffer;
                //像写文件一样向sockfd中写(发送消息)
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)//表示读到了文件结尾,表示客户端结束
            {
                LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
                break;
            }
            else
            {//读出错了
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);//关文件描述符
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;//监听套接字
    bool _isrunning;
};

TcpServerMain.cc

#include "TcpServer.hpp"

#include <memory>


// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    //port参数被传递给TcpServer的构造函数,
    //以便服务器知道在哪个端口上监听连接请求。
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}


2.4客户端逻辑(代码) 

TcpClientMain.cc代码主体逻辑:

  1. 参数检查:首先,程序检查命令行参数的数量是否正确。它需要两个额外的参数:服务器的IP地址和端口号。如果参数数量不正确,程序会打印使用说明并退出。

  2. 创建Socket:通过调用socket()函数创建一个TCP socket(即使用SOCK_STREAM作为socket类型)。如果socket创建失败,程序会打印错误信息并退出。

  3. 设置服务器地址信息:程序接着使用sockaddr_in结构体来存储服务器的地址信息。它首先清零整个结构体,然后设置地址族为AF_INET(IPv4),使用htons()函数将端口号从主机字节序转换为网络字节序,并使用inet_pton()函数将服务器IP地址的字符串形式转换为网络字节序的二进制形式。

  4. 连接到服务器:通过调用connect()函数尝试连接到服务器。这个函数需要socket文件描述符、指向服务器地址的指针以及地址的长度作为参数。如果连接失败,程序会打印错误信息并退出。

  5. 与服务器通信:一旦连接成功,程序进入一个无限循环,等待用户从标准输入(stdin)输入消息。对于每次输入的消息,程序使用write()函数将其发送到服务器。然后,程序使用read()函数从socket读取服务器发送回来的响应。如果读取到的字节数大于0,程序会将接收到的数据作为字符串打印到标准输出(stdout)。如果读取到的字节数为0或发生错误(例如连接被关闭),则跳出循环。

  6. 关闭Socket:在退出循环后,程序使用close()函数关闭socket文件描述符,以释放与socket相关的资源。

注意,read()函数没有处理EAGAINEWOULDBLOCK错误,这些错误可能发生在非阻塞socket上。然而,在这个例子中,socket是默认创建的,因此它是阻塞的,所以这些错误不太可能发生。如果希望处理非阻塞socket或超时,则需要在socket()调用后设置socket为非阻塞模式,并在read()调用时处理可能的错误情况。

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ./tcpclient server-ip server-port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    //服务器端口号是固定的,但是客户端不是,因为客服是随机的
    // client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?
    // client 需要 bind它自己的IP和端口, 但是client 不需要 “显示指明” bind它自己的IP和端口, 
    // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口,
    //避免端口冲突

    //填充服务器的相关信息
    struct sockaddr_in server;//服务器的套接字信息
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);//要把主机序列转为网络序列
    // inet_addr把字符串形式的ip转为4字节(进程序列转网络序列)
   ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
   //客户端需要调用 connect()连接服务器;
    int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {//连接失败
        std::cerr << "connect socket error" << std::endl;
        exit(2);
    }

    while(true)//连接成功后,客户端与服务端进行通讯
    {
        std::string message;
        std::cout << "Enter #";
        std::getline(std::cin, message);
        //发消息
        write(sockfd, message.c_str(), message.size());

        char echo_buffer[1024];

        //读消息
        n = read(sockfd, echo_buffer, sizeof(echo_buffer));
        if(n > 0)
        {
            echo_buffer[n] = 0;
            std::cout << echo_buffer << std::endl;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
    return 0;
}


2.5代码测试 

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值