【计算机网络】socket编程 --- 实现简易TCP网络程序

在这里插入图片描述

👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


一、 服务端

1.1 前言

这里我们规定将TCP服务器封装成一个类,以下是服务器程序框架

tcpServer.cc

#include <iostream>
#include "tcpServer.hpp"
#include <memory>
using namespace std;

int main()
{
   
    // 1. 创建TCP服务器端对象
    unique_ptr<tcpserver> tcpsvr(new tcpserver());
    // 2. 初始化TCP服务器
    tcpsvr.Init();
    // 3. 启动TCP服务器
    tcpsvr.Run();

    return 0;
}

tcpServer.hpp

#pragma once
#include <iostream>
#include "log.hpp"

log lg;

class tcpserver
{
   
public:
    tcpserver()
    {
   }

    ~tcpserver()
    {
   }

    void Init()
    {
   }

    void Run()
    {
   }

private:
};

说明:lg是我往期封装的日志类对象,这个在UDP也使用过。具体可以参考一下文章:

1.2 创建套接字

要想进行网络通信,第一步就是要先创建套接字socket

【函数原型】

#include <sys/types.h>           
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

说明:

  • domain:指定套接字的协议族(如 AF_INET 表示IPv4AF_INET6表示IPv6网络、AF_UNIX / AF_LOCAL用于本地进程间通信)。
  • type:指定套接字的类型(如 SOCK_STREAM 表示TCPSOCK_DGRAM 表示UDP)。
  • protocol:指定具体的协议,通常设置为 0 表示使用默认协议。

综上,TCP服务器在创建套接字时,参数设置如下:

  • 参数一:因为我们要进行的是网络通信,协议家族选择AF_INET

  • 参数二:因为我们编写的是TCP服务器,所以选择SOCK_STREAM

  • 参数三:协议类型默认设置为0即可。

tcpServer.hpp

#pragma once

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

log lg;

enum
{
   
    SOCK_ERR = 1,
};

class tcpserver
{
   
public:
    tcpserver()
        : _socketfd(-1)
    {
   }

    ~tcpserver()
    {
   
         if (_socketfd >= 0)
         {
   
             close(_socketfd);
         }
    }

    void Init()
    {
   
        // 创建套接字
        _socketfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_socketfd == -1) // 套接字创建失败
        {
   
            lg.logmessage(Fatal, "create socket. errno: %d, describe: %s", errno, strerror(errno));
            exit(SOCK_ERR);
        }
        lg.logmessage(Info, "create socket. errno: %d, describe: %s", errno, strerror(errno));

    }

private:
    int _socketfd; // 套接字文件描述符

};

说明:

  • 实际TCP服务器创建套接字的做法与UDP服务器是一样的,只不过创建套接字时TCP需要的是流式服务,即SOCK_STREAM;而UDP需要的是用户数据报服务,即SOCK_DGRAM

  • 当析构服务器时,可以将服务器对应的文件描述符进行关闭。也可以选择不关闭。因为操作系统会在进程结束时自动回收文件描述符。然而,为了避免资源泄露和潜在的文件描述符耗尽问题,最好在程序退出前手动关闭文件描述符。这样可以更好地控制资源的释放。

1.3 绑定套接字

套接字创建完毕后,实际只是在系统层面上打开了一个文件(网卡文件),该文件还没有与网络关联起来,因此创建完套接字后我们还需要进行绑定操作bind

【函数原型】

#include <sys/types.h>         
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

说明:

  • sockfd:套接字的文件描述符,通常是由 socket 函数返回的。
  • addrsockaddr结构体是一个套接字的通用结构体,实际我们在进行网络通信时,还是要定义sockaddr_insockaddr_un这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*
    • sockaddr_in结构体:用于跨网络通信
    • sockaddr_un结构体:是用于本地通信
    • 注意:使用以上结构体需要加上头文件<netinet/in.h>
  • addrlenaddr 结构体的大小,以字节为单位。
  • 返回值:成功返回 0;失败返回 -1,并设置errno以指示错误原因。

在这里插入图片描述


首先对于bind函数,第一个参数和第三个参数没的说。主要是第二个参数。因为是跨网络通信,我们需要定义struct sockaddr_in结构体。而该结构体具体有哪些成员呢?我们可以在vscode中查看sockaddr_in结构体的相关成员:

在这里插入图片描述

注意:因为该结构体中还有其他字段,我们可以不用管。因此,可以使用 memset 函数可以确保结构体的所有字段都被初始化为零,从而避免意外数据。

#include <string.h>
void *memset(void *s, int c, size_t len);
// s是指向要填充的内存区域的指针
// c是要填充的值
// len是要填充的字节数

// -------------------------
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 将 addr 结构体的所有字节设置为零

这里再介绍一个函数bzero,它的作用是一个用于将内存区域的字节设置为零的函数。

#include <strings.h> 
void bzero(void *s, size_t len);

初始化之后,我们需要设置struct sockaddr_in成员变量,比如端口号和IP地址之类的

  • sin_family:表示协议家族。必须使用与socket创建时相同的协议家族。例如,如果你使用AF_INET 创建了套接字,bind时也应使用AF_INET
  • sin_port:表示端口号,是一个16位的整数。注意:端口号需要转换为网络字节序。因为只要进行网络通信,端口号一定是双方来回传输的数据,因此为了保证双方能够正常解析数据,需要将其转换为网络字节序。可以使用htons函数
#include <arpa/inet.h>  
 
uint16_t htons(uint16_t hostshort);
  • sin_addr:其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,就是IP地址。我们用户最直观的就是输入类似于192.168.1.1这种字符串形式的IP地址,可是这里sin_addr的类型是32位无符号整数,那么我们要把字符串转化为32位无符号整数。并且在网络通信中,IP地址和端口号一样,也是双方来回传输的数据,也要保证双方能够正常解析数据。由于这些操作在网络中经常会用到,我们并不需要自己手动实现一遍,系统已经我们提供了一些函数:

在这里插入图片描述

对于IP地址,我们可以先将其设置为本地环回127.0.0.1来进行本地通信测试;当然也可以设置为公网IP地址,表示网络通信。

但需要注意的是:

  • 如果你使用的是虚拟机,那么可以设置为公网IP地址;如果使用的是云服务器,那么在设置服务器的IP地址时,不需要显示绑定IP地址,但可以设置成本地环回地址,当然也可以直接设置为INADDR_ANY,这是个宏函数,本质上就是0.0.0.0。此时服务器就可以从本地任何一张网卡当中读取数据。
  • 另外,以上绑定只是在用户层上绑定了,填充完服务器网络相关的属性信息后,需要调用bind函数进行内核绑定。绑定实际就是将文件与网络关联起来,如果绑定失败也没必要进行后续操作了,直接终止程序即可。
#pragma once

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

log lg;

enum
{
   
    SOCK_ERR = 1,
    BIND_ERR
};

class tcpserver
{
   
public:
    tcpserver(const uint16_t &port, const std::string &ip = "0.0.0.0")
        : _socketfd(-1), _port(port), _ip(ip)
    {
   }

    void Init()
    {
   
        // 1. 创建套接字
        _socketfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_socketfd == -1) // 套接字创建失败
        {
   
            lg.logmessage(Fatal, "create socket. errno: %d, describe: %s", errno, strerror(errno));
            exit(SOCK_ERR);
        }
        lg.logmessage(Info, "create socket. errno: %d, describe: %s", errno, strerror(errno));

        // 2. 绑定套接字
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 细节
        inet_aton(_ip.c_str(), &(local.sin_addr)); // 细节
        
        // 内核绑定
        int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0) // 绑定失败
        {
   
            lg.logmessage(Fatal, "bind socket. errno: %d, describe: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg.logmessage(Info, "bind socket. errno: %d, describe: %s", errno, strerror(errno));
    }

private:
    int _socketfd; // 套接字文件描述符
    uint16_t _port; // 服务器端口号
    std::string _ip; // 服务器IP地址
};

代码写到目前为止,虽然细节很多,但其实和UDP前半部分一样!都是套路!

而接下来就和UDP不一样了,因为TCP是一个面向连接的,所以在正式通信之前,一定要客户端和服务器连接上再说,即要将套接字设置为监听状态。因为只有设置为监听状态,才能知道有别人要来和我建立连接,然后基于连接再进行通信。

1.4 监听

listen 函数用于将套接字设置为监听状态,以等待传入的连接请求。它通常用于服务器端,配合 socketbind 函数使用

【函数原型】

#include <sys/types.h>         
#include <sys/socket.h>

int listen(int sockfd, int backlog);                                                                                                                                                                     

说明:

  • sockfd: 是之前通过 socket 函数创建的套接字描述符。
  • backlog: 指定套接字的待连接队列的最大长度(数目)。意思就是说,如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度。注意:一般不要设置太大,设置为510即可。
  • 返回值:成功返回0,失败返回-1,并设置 errno 以指示错误类型。
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值