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

2024-09-18 08:10:53 浏览数 (1)

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

1 TCP与UDP

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

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

TCP

  1. TCP 是面向连接的协议,意味着在数据传输之前,必须先建立一个连接,完成握手过程。这个连接在数据传输结束之后需要被断开。
  2. TCP 提供了可靠的服务。它确保数据包的顺序传输,并且通过确认(ACK)和重传机制保证数据的可靠性。
  3. TCP 因为需要建立连接、保证数据顺序和可靠性,所以传输速度相对较慢。
  4. TCP 将数据视为一个连续的数据流,确保数据按照发送的顺序到达。
  5. TCP 适用于要求高可靠性的应用,如网页浏览、文件传输(FTP)、电子邮件(SMTP)等。
  6. TCP 头部较大,因为它需要包含更多的信息来管理连接状态和保证数据的可靠性。

UDP

  1. UDP 是无连接的,它发送数据之前不需要建立连接,每个数据报文都是一个独立的信息传输单位。
  2. UDP 不保证数据包的顺序,也不保证数据包的可靠性。如果数据在传输过程中丢失,UDP不会进行重传。
  3. UDP 由于无需建立连接和保证可靠性,通常用于对实时性要求较高的应用,如视频会议和在线游戏,传输速度较快。
  4. UDP 将数据视为独立的、离散的数据包(datagrams),每个数据包独立处理,可能以不同的顺序到达。
  5. UDP 头部较小,处理起来更为高效。
  6. UDP 适用于实时性要求高的应用,如流媒体、实时视频会议(VoIP)、在线游戏等。

通俗理解的话: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:根据获取的连接流和发送者的信息开始读取接收数据
代码语言:javascript复制
#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函数进入监听状态。

初始化任务就完成了

代码语言:javascript复制
// 进行初始化
    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: %dn", _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,文件流中我们对他们很熟悉!!!
代码语言:javascript复制
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 errorn");
                continue;
            }
            InetAddr addr(client);
            // 读取数据
            LOG(INFO, "get a new link, client info : %s, sockfd is : %dn", 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: %sn" , 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: %sn", addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }

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

3 服务端与客户端

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

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

代码语言:javascript复制
#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进行写入与读取了!!!
代码语言:javascript复制
#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 人点赞