<网络>TCP代码协议编写

2023-07-09 1900阅读

温馨提示:这篇文章已超过401天没有更新,请注意相关的内容是否还可用!

首先,我们需要了解什么是TCP。TCP是一种传输控制协议。首先,我们要实现客户端和服务器之间的网络通信。答案是每台主机的网络IP。当我们的电脑访问互联网时,它会自动为我们的电脑分配一个局域网中的IP。网络通信底层是通过硬件网卡进行的。所以这里为了方便起见,我们先封装一个Sock类,以便更好的编写服务端。TCP中的这个fd也是一个sock套接字,称为监听套接字

目录

1、网络通信的本质是什么?

本篇文章我们主要实现如何使用TCP进行完整的网络通信

首先,我们需要了解什么是TCP。 TCP是一种传输控制协议。 现在让我们记住这一点。

现在我们先开始吧。 首先,我们要实现客户端和服务器之间的网络通信。 两个不同主机之间的通信是什么? 答案是每台主机的网络IP。 当我们的电脑访问互联网时,它会自动为我们的电脑分配一个局域网中的IP。

好了,现在我们的两台主机已经找到了对方,接下来我们就看看我们要做什么,两台主机之间的通信是否正确? 说清楚一点,我们需要使用一台主机上的APP向另一台主机上的APP发送消息,而APP在电脑上,启动的时候叫什么? 我们为那些正在运行的应用程序使用一个更专业的名称,称为进程! ! ! ok,就是我们需要大家明白我们到底想要达到什么目的! -- 两台主机中两个进程之间的通信! ! ! 好了,这段话明白到这里了。 流程类似下图:

<网络>TCP代码协议编写

接下来我们要介绍一个新的概念:端口号,大家首先都承认有这个东西,之前我们都知道,现在两台主机通过IP可以找到对方,现在的问题是:一台主机不能只永远运行一个进程,就像我们同时启动QQ、微信、腾讯视频、浏览器等,假设我们要在两台主机之间进行QQ通信,如何才能在这么多进程中成为唯一的一个并坚定地选择它呢? ? ,这个问题很关键。 那么我们的设计师是如何解决这个问题的呢? 接下来就是重点了:我们可以给每个进程绑定一个端口号(如何绑定代码会体现),并且规定一个端口号只能绑定一个进程,注意:这里一个进程可以绑定这个时候我们在网络上通信的时候,只需要带上对方的主机IP和进程绑定的端口号,就可以准确的进行网络通信了。 是不是很简单啊哈哈哈

二、编写代码--------

那么我们需要在代码中做什么呢?

1.我们需要获得一个IP,对吧?

2、我们需要自定义一个端口号,并绑定这个IP,形成唯一的对应关系。 这里的关系是生成一个socket套接字,即:IP+port(端口号)。

这样我们就可以实现通信的一般逻辑了。 网络通信底层是通过硬件网卡进行的。 这些硬件的操作需要很高的权限,所以显然,网络相关的接口都是在系统级别的! ! !

所以这里为了方便起见,我们先封装一个Sock类,以便更好的编写服务端。

Sock有哪些功能? 本课是关于TCP协议的,我们需要了解TCP通信的细节:

TCP Sock服务编写流程---

1、创建sock套接字:使用socket函数获取网络的fd文件描述符。 TCP中的这个fd也是一个sock套接字,称为监听套接字

2、绑定IP+端口号:通过bind函数将此fd描述符与IP+端口号绑定,实现网络通信的必要条件

3、设置监听状态:通过listen函数将这个fd,也就是sock套接字设置为监听状态,这个sock也称为监听套接字

4、接受连接:再次使用accept函数,通过传入监听的socket和一个struct sockaddr_in类型的输入输出参数来接受客户端的连接请求,如果连接成功则返回一个socket,两个进程网络通信是通过这个新的套接字完成的!

解释一下第3点和第4点,因为TCP是面向连接的,所以当我们正式通信的时候,我们需要先建立连接,那么我们的服务器是不是需要处于等待我们连接的状态呢? 第二个状态是监听状态。 我举个例子方便大家理解。 就像我们去一些餐馆吃饭一样,白天去的时候总是可以吃的。 为什么? 因为那些餐馆的老板总是在等待客人的到来,以便能够及时为客人服务,所以监听状态就是这样,而用来设置监听状态的套接字称为监听套接字。

好了,现在大体流程已经清楚了,那么另一个问题是,我们如何理解监听的socket和新返回的socket呢? !

我们讲一个小故事方便大家理解:假设,你现在去一个地方旅行,你去一个地方吃饭。 这个地方有很多餐厅,每个餐厅都比较内向,雇佣一个人。 这个人什么都不做,只做一件事,那就是去路边招揽顾客,路边就会有人招揽顾客,并把顾客带到店里。 假设顾客是你,这个律师叫李四,李四拉你去店里。 你答应了,于是李四就带你去了餐厅,然后大声喊道,“一个物流服务员出来了,有客人,他跟你打招呼”,于是出来一个服务员张三,于是张三就来给你服务。 当张三为你服务时,李四就跑到路边招揽顾客,如此循环往复。

上面的例子:

监听socket---拉拢李四,工作就是获取新链接

新的socket返回了---是服务员张三

到这里相信大家应该明白这两个套接字的作用和意义了。

通过上面的过程,就实现了两个连接,我们就可以开始通信了!

<网络>TCP代码协议编写

可以直接使用read和write来读写socket。

3.编写一个Sock类

让我们开始战斗吧! ! !

首先写一个Sock类,其中包含

1.创建sock套接字

2. 绑定IP+端口号

3. 设置监控状态

4. 接受连接

以上四种基本方法

//写一个Scok类
class Sock
{
public:
    Sock(){} // 无用
    int Socket(){} //1.创建套接字
    void Bind(int sock, std::string ip, uint16_t port){}  //2.实现套接字和IP、端口号的绑定
    void Listen(int listensock){} //3.设置监听套接字
    int Accept(){int listensock, std::string& ip, uint16_t& port}  //4.建立连接
    ~Sock(){} // 无用
};

Socket() 实现

基于以上知识,我们需要调用socket系统函数来获取socket,如下

AF_INET:网络系列,使用IPV4

SOCK_STREAM:使用流套接字,TCP是面向字节流的

0:默认即可

int Socket()
{
     int _listensock = socket(AF_INET, SOCK_STREAM, 0);
     if (_listensock < 0)
     {
        logMessage(FATAL, "creat listensocke [%d]-[%s]", errno, strerror(errno));
        exit(-1);
     }
     logMessage(NORMAL, "creat listensocke success is : %d",_listensock);
 
     return _listensock;
}

void Bind(int sock, uint16_t port, std::string ip) 的实现

bind系统函数需要使用sockaddr_in结构体,我们可以直接定义初始化参数

// 2.绑定IP+Port
void Bind(int sock, uint16_t port, std::string ip)
{
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    { 
        logMessage(FATAL, "bind listensocke [%d]-[%s]", errno, strerror(errno));
        exit(1);
    }
    logMessage(NORMAL, "bind listensocke success!");
}

void Listen(int Listensock) 的实现

这个gblock,以我目前的写作水平还无法详细回答。 到目前为止我所知道的是默认整数 20 就足够了。

// 3.设置监听状态
void Listen(int listensock)
{
    if (listen(listensock, gblock) < 0)
    {
        logMessage(FATAL, "set listensocke to listen status fail [%d]-[%s]", errno, strerror(errno));
        exit(1);
    }
    logMessage(NORMAL, "set listent status success");
}

int Accept(int Listensock, std::string& ip, uint16_t& port) 的实现

具体是,如果连接失败,就进行下一次连接,就像李四没接到顾客,就去找下一个顾客一样,不能直接关店。

// 4.连接
int Accept(int listensock, std::string& ip, uint16_t& port)
{
    struct sockaddr_in addr;
    socklen_t len = sizeof addr;
    while (true)
    {
        int serverock = accept(listensock, (struct sockaddr*)&addr, &len);
        if (serverock < 0)
        {
            logMessage(ERROR, "get accept fail [%d]-[%s]", errno, strerror(errno));
        }
        else
        {
            logMessage(NORMAL, "accpet success [%d]-[%s]", errno, strerror(errno));
            ip = inet_ntoa(addr.sin_addr);
            port = ntohs(addr.sin_port);
            
                return serverock;
        }
    }
}

四、Tcp_Server类的编写

我们刚刚封装了Sock,现在写这个类就容易多了。

<网络>TCP代码协议编写

我们直接大招,看看下面的代码是不是很简单


class Tcp_server
{
public:
    Tcp_server(std::string ip = "", uint16_t port = 0)
    :_ip(ip), _port(port)
    {
        Sock c_server;
        // 1.创建监听套接字
        _listensock = c_server.Socket();
        // 2.绑定IP+Port
        c_server.Bind(_listensock, _port, _ip);
        // 3.设置监听状态
        c_server.Listen(_listensock);
    }
    void Start()
    {
        Sock c_server;
        // 4.连接
        while (true)
        {
            std::string ip;
            uint16_t port;
            int serversock = c_server.Accept(_listensock, ip, port);
            // 开始服务 
            server(serversock, ip, port); //服务函数等下再编写
            close(serversock);
        }
    }
    ~Tcp_server()
    {
        close(_listensock);
    }
private:
    std::string _ip;
    uint16_t _port;
    int _listensock;
};

附上服务器功能:这里实现的是一个很简单的逻辑,客户端发送给服务器,服务器返回。

你注意到了吗? 我们在系统文件级别使用 write 和 read 来操作套接字。 这里我们可以看到socket是文件描述。 网络和Linux中的一切都是文件,真的吗? ! ! ! 它呼应了它!


void server(int sock, std::string ip, uint16_t port)
{
    char serverBuffer[1024];
    while (true)
    {
        ssize_t s = read(sock, serverBuffer, (sizeof serverBuffer) - 1);
        if (s < 0)
        {
            logMessage(ERROR, "read message fail [%d]-[%s]", errno, strerror(errno));
            break;
        }
        else if (s == 0)
        {
            logMessage(FATAL, "读端关闭");
            break;
        }
        else if (s > 0)
        {
            serverBuffer[s] = '\0';
            std::cout << "Client --- IP:" << ip << "Port:" << " " << port << std::endl;
            std::cout << serverBuffer << std::endl;
        }
        // 发送回去
        write(sock, serverBuffer, sizeof serverBuffer - 1);
    }
}

5. 实现来回发送消息的服务器和客户端

最后我们看看并实现一个简单的客户端和服务器

第一的

#include 
#include 
#include "Tcp_server.hpp"
void Usage(std::string s)
{
    std::cout << "Usage "<< s <<  " port" << std::endl;
}
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(-1 );
    }
    std::string ip = "0.0.0.0";
    uint16_t port = atoi(argv[1]);
    Tcp_server server(ip, port);
    server.Start();
    return 0;
}

注意,客户端不需要自己绑定IP和端口号,它会自动分配,因为如果我们自己绑定的话,可能会出现端口号冲突,从而导致另一个客户端无法启动的情况。

#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"
void Usage(std::string s)
{
    std::cout << "Usage "<< s <<  "ServerIp ServerPort" << std::endl;
}
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(-1);
    }
    // 编写客户端逻辑
    // 1.想要连接的IP
    // 2.想要连接的端口
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    struct sockaddr_in addr;
    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());
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    // 客户端会自动分配端口号
    if (connect(sock, (struct sockaddr*)&addr, sizeof addr) < 0)
    {
        logMessage(FATAL, "client connect fail [%d]-[%s]", errno, strerror(errno));
        exit(-1);
    }
    logMessage(NORMAL, "connect success [%d]-[%s]", errno, strerror(errno));
    while (true)
    {
        std::string line;
        std::cout << "输入发送消息:";
        std::getline(std::cin, line);
        // write(sock, line.c_str(), sizeof line.c_str());
        send(sock, line.c_str(), line.size(), 0);
        
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "recv server message:" << buffer << std::endl;
        }
    }
    return 0;
}

编译完成后,运行它们可以看到:

这里使用Centos7.6版本的操作系统

首先运行服务器,注意这里我们没有指定服务器是哪个IP地址,因为我们在代码中使用的默认IP“0.0.0.0”表示服务器的任何IP

87ae6c829143fb9f31339a3ac4afab52

按回车运行后,我们再次启动客户端

171be4527f77ebe2293e63bd24bff0ef

运行后是这样的

04658f8b8902351a164de78231f29595

让我们来看看

dd072994ffe1c4f72118cb8222e8d969

我们发消息试试

c842994116942831e97dd637424c9ec7

可以看到我们成功发送了消息,服务器也返回了消息,到这里我们就实现了一个简单的TCP协议网络通信服务器;

六、附加日志功能

代码中的logMessage是我自己写的一个用于打印日志信息的函数,分享给大家

#pragma once
#include 
#include 
#include 
#include 
#include 
#define DEBUG   0
#define NORMAL   1
#define WARNING  2
#define ERROR    3
#define FATAL    4
#define DEBUG_SHOW
const char* gLevel[]  = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};
void logMessage(int level, char* format, ... )
{
#ifndef DEBUG_SHOW
    if (level == DEBUG) return; 
#endif
    // 标准
    char stdBuffer[1024];
    const time_t curt = time(nullptr);
    struct tm* _curt = localtime(&curt);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s]-[%d年%d月%d日%d时%d分%d秒]", gLevel[level], 
            _curt->tm_year + 1900, _curt->tm_mon + 1, _curt->tm_mday, ((_curt->tm_hour + 12) % 24) + 12, _curt->tm_min, _curt->tm_sec);
    // 自定义
    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsprintf(logBuffer, format, args);
    va_end(args);
    FILE* fd = fopen("log_tcp_server", "w");
    fprintf(fd, "%s-%s\n", stdBuffer, logBuffer);
    fclose(fd);
}

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]