简单的TCP网络程序:英译汉服务器

一、服务器的初始化

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

1.创建套接字

socket():

⭐参数介绍:

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果socket()调用出错则返回-1;
  • 对于IPv4, family参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可。
class TcpServer
{
public:
    TcpServer()
        : _sockfd(defaultsockfd)
    {
    }
    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0) // 创建套接字失败
        {
            lg(Fatal, "create socket, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, socket: %d", _sockfd);
    }
    ~TcpServer()
    {
        close(_sockfd);
    }

private:
    int _sockfd; // 套接字
};

2.绑定端口号和ip

bind():

⭐参数介绍:

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

我们先来看看地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址 但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢?

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问 题;

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

  • 1. 将整个结构体清零;
  • 2. 设置地址类型为AF_INET;
  • 3. 网络地址为0.0.0.0, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址;
class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
        : _sockfd(defaultsockfd)
        , _port(port)
        , _ip(ip)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0) // 创建套接字失败
        {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, socket: %d", _sockfd);

        //2.绑定端口号
        // 使用这个结构体需要包头文件
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // 转化为网络序列
        local.sin_port = htons(_port);
        // 字符串转化为点时分形式的ip
        inet_aton(_ip.c_str(), &(local.sin_addr));
        //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
        int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
    }
    ~TcpServer()
    {
        close(_sockfd);
    }

private:
    int _sockfd; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
};

3.设置监听状态

listen():

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

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
        : _sockfd(defaultsockfd)
        , _port(port)
        , _ip(ip)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0) // 创建套接字失败
        {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, socket: %d", _sockfd);

        //2.绑定端口号
        // 使用这个结构体需要包头文件
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // 转化为网络序列
        local.sin_port = htons(_port);
        // 字符串转化为点时分形式的ip
        inet_aton(_ip.c_str(), &(local.sin_addr));
        //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
        int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, socket: %d", _sockfd);


        // 3.设置监听状态
        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if (listen(_sockfd, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }   
        lg(Info, "bind socket success, socket: %d", _sockfd);

        
    }
    ~TcpServer()
    {
        close(_sockfd);
    }

private:
    int _sockfd; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
};

4.设置服务器的端口号和ip

未来我们向让用户设置端口号,所以我们可以在main函数中借助命令行参数传递,对于ip我们就直接使用默认的ip参数"0.0.0.0"即可。

#include "TcpServer.hpp"

#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    unique_ptr<TcpServer> tcpSvr(new TcpServer(port));
    tcpSvr->Init();
    //tcpSvr->Start();
    return 0;
}

现在我们就可以来测试一下啦!

二、服务器的运行

1.建立新链接

accept():

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

⭐accept函数返回的套接字是什么?

调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。

监听套接字与accept函数返回的套接字的作用:

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,类似于餐厅门口的迎宾,而真正为这些连接提供服务的套接字是accept函数返回的套接字,类似于餐厅的服务员,而不是监听套接字。

所以初始化TCP服务器时创建的套接字应该叫做监听套接字。为了表明寓意,我们将代码中套接字的名字由_sockfd改为_listensocket,这样写着更清楚明了。

#pragma onec

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;

const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;

enum
{
    UsageError = 1,
    SocketError = 2,
    BindError = 3,
    ListenError = 4
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port, const string& ip = defaultip)
        : _listensocket(defaultsockfd)
        , _port(port)
        , _ip(ip)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _listensocket = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensocket < 0) // 创建套接字失败
        {
            lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, socket: %d", _listensocket);

        //2.绑定端口号
        // 使用这个结构体需要包头文件
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // 转化为网络序列
        local.sin_port = htons(_port);
        // 字符串转化为点时分形式的ip
        inet_aton(_ip.c_str(), &(local.sin_addr));
        //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
        int n = bind(_listensocket, (const struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, socket: %d", _listensocket);


        // 3.设置监听状态
        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if (listen(_listensocket, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }   
        lg(Info, "bind socket success, socket: %d", _listensocket);
    }

    void Start()
    {
        lg(Info, "tcpServer is running....");
        for (;;)
        {
            // 1. 获取新连接 - 知道客户端的ip地址和端口号
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾 
            // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
            int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue; // 所以这里使用continue
            }
            lg(Info, "get a new link..., sockfd: %d", sockfd);
        }
    }

    ~TcpServer()
    {
        close(_listensocket);
    }

private:
    int _listensocket; // 套接字
    uint16_t _port; // 端口号
    string _ip; // ip地址
};

此时我们想来测试一下,但是我们的客户端还没有写,咋办呢?

telnet 127.0.0.1 8888 

是一个网络诊断命令,它的作用是尝试通过Telnet协议连接到本地主机(本机)的8888端口。这里:

  • telnet 是命令本身,用于进行远程登录和管理。
  • 127.0.0.1 是IPv4环回地址,指向本地计算机。使用这个地址,你实际上是尝试连接到你自己的机器。
  • 8888 是端口号,许多应用程序和服务会监听特定的端口来接收数据。8888是一个常见的测试或备用端口。

上一个知识点我们提到udp它是不能绑定我们云服务器的公网ip的,但是绑定本地环回127.0.0.1可以的,我们看看tcp可不可以。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值