0
点赞
收藏
分享

微信扫一扫

【计网】从零开始使用TCP进行socket编程 --- 客户端与服务端的通信实现

elvinyang 2024-09-23 阅读 18

在这里插入图片描述

阵雨后放晴的天空中,

出现的彩虹很快便会消失。

而人心中的彩虹却永不会消失。

--- 太宰治 《斜阳》---


从零开始使用TCP进行socket编程

1 TCP与UDP

我们之前实现了UDP协议下的客户端与服务端的通信。

UDP(用户数据报协议)和TCP(传输控制协议)都是网络通信中常用的传输层协议,它们在数据传输的方式和特性上存在以下特点:

TCP

UDP

通俗理解的话:TCP的传输过程类似管道,数据从一端发送,然后在另一端按顺序接收。UDP传输数据的过程类似送快递,数据报文会一股脑包装在一起发送给接收者!

2 TCP服务器类

2.1 TCP基础知识

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

2.2 整体框架设计

下面我们就来设计一下TCP协议下的服务器类:

  1. 成员变量需要整体通信的_listensockfd和端口号_port,后续绑定网络通信接口,从中读取连接流。
  2. 初始化接口InitServer:对端口号进行绑定,将网络通信接口设置为"接听"模式,可以获取外部的链接。
  3. 循环读取接口Loop:从网络通信接口获取连接流与发送者的信息,之后进行数据接收。
  4. 服务端口Service:根据获取的连接流和发送者的信息开始读取接收数据
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <memory>
#include <string>
#include <cstring>
#include <iostream>
#include <functional>
#include <unistd.h>

#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;
//基础信息
const int gport = 8888;
const int gblocklog = 8;
//错误码
enum
{
    SOCKET_FD = 1,
    SOCKET_BIND,
    SOCKET_LISTNE
};

class TcpServer
{
public:
    TcpServer(int port = gport) : _port(port),
                                  _listensockfd(-1),
                                  _isrunning(false)
    {
    }
    // 进行初始化
    void InitServer()
    {   
    }
    void Loop()
    {
       
    }
    void Service(int sockfd, InetAddr addr)
    {
        
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;    // 服务器端口
    int _listensockfd; // 链接文件
    bool _isrunning;
};

这就是基础的框架。

2.3 初始化接口

InitServer()初始化接口进行的工作很好理解:

  1. 首先创建socket文件,获取到_listensockfd
  2. 然后将服务器结构体的成员进行初始化,将服务器端口与_listensockfd进行绑定
  3. 最后将_listensockfd通过listen函数进入监听状态。

初始化任务就完成了

// 进行初始化
    void InitServer()
    {
        // 创建socket文件 --- 字节流方式
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "socket error!!!\n");
            exit(SOCKET_FD);
        }
        LOG(INFO, "socket create success!!! _listensockfd: %d\n", _listensockfd);
        // 建立server结构体
        struct sockaddr_in local;
        memset(&local , 0 , sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY; // 服务器IP一般设置为0
        local.sin_port = htons(_port); //一定注意主机序列转网络序列
        

        // 进行绑定
        if (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LOG(FATAL, "bind error!!!\n");
            exit(SOCKET_BIND);
        }
        LOG(INFO, "bind success!!!\n");

        // 将_listensockfd文件转换为listening状态!!!
        if (::listen(_listensockfd, gblocklog) < 0)
        {
            LOG(FATAL, "listen error!!!\n");
            exit(SOCKET_LISTNE);
        }
        LOG(INFO, "listen success!!!\n");
    }

2.4 循环接收接口与服务接口

Loop()循环接收接口需要:

  1. 不断从套接字文件中accept获取连接流与客户端信息!
  2. 获取成功后,就可以进行服务了
  3. 服务就是从流中读取数据,然后处理之后再写回流中!!!使用的接口是read与write,文件流中我们对他们很熟悉!!!
void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // accept接收sockfd
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);
            if(sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            InetAddr addr(client);
            // 读取数据
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);

            // version 0 --- 不靠谱版本
            Service(sockfd, addr);
        }
        _isrunning = false;
    }
void Service(int sockfd, InetAddr addr)
    {
        LOG(INFO , "service start!!!\n");
        while (true)
        {
            char buffer[1024];
            ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);
            

            if (n > 0)
            {
                buffer[n] = 0;
                LOG(INFO , "sockfd read success!!! buffer: %s\n" , buffer);

                std::string str = "[server echo]#";
                str += buffer; 
                write(sockfd, str.c_str(), str.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);
    }

这样基础的服务器的通信工作就写好了

3 服务端与客户端

接下来我们来完善一下服务端和客户端的通信逻辑,让他们可以通信起来

服务端简单的创建一个服务器类然后进行初始化和loop就可以了!!!

#include "TcpServer.hpp"

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]);

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

客户端稍微复杂一些:

  1. 首先根据传入的参数进行初始化服务器IP地址和端口号
  2. 然后创建套接字文件 ,并进行connect连接绑定bind,客户端回被动绑定一个端口号!!!
  3. 绑定成功之后就可以通过sockfd进行写入与读取了!!!
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <memory>
#include <string>
#include <cstring>
#include <iostream>

#include "Log.hpp"

using namespace log_ns;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

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

    // 创建socket文件
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        LOG(FATAL, "sockfd create error!!!\n");
        exit(1);
    }

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 数据归零
    server.sin_family = AF_INET;
    server.sin_port = htons(port); // 端口号 主机序列转网络序列!!!
    ::inet_pton(AF_INET, ip.c_str(), &server.sin_addr);//安全写入

    // 进行发送数据
    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 line;
        std::cout << "Please Enter: ";
        std::getline(std::cin, line);

        ::write(sockfd, line.c_str(), line.size());
        LOG(DEBUG , "write success !!!\n");
        // 读取数据
        char buffer[1024];
        int n = read(sockfd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
    return 0;
}

测试运行

我们来测试一下服务端和客户端是否可以做到通信:
在这里插入图片描述
很好,可以完美的进行通信!!!

之后我们就可以加入多线程,加入回调函数逻辑,就可以进行业务处理了!!!

举报

相关推荐

0 条评论