Linux之Socket编程Tcp

一、Tcp网络编程

1.1、EchoServer

接口介绍:

// 开始监听 socket (TCP, 服务器)

int listen(int socket, int backlog);

  • 参数:
    • socket:已绑定 (bind()) 的TCP套接字描述符(必须为 SOCK_STREAM 类型)
    • backlog:等待连接队列的最大长度,即允许挂起(未 accept())的连接请求数量,不可以为0,也不可以太大。
  • 返回值:
    • 成功 :返回 0

    • 失败:返回 -1,并设置 errno。

// 接收请求 (TCP, 服务器)

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

  • 参数:
    • socket:服务器监听套接字(由 socket() 创建且已调用 bind() 和 listen())
    • address:可选参数,用于存储客户端的地址信息(IP + 端口)
    • address_len:输入时指定 address 缓冲区长度,输出时返回实际地址长度
  • 返回值:
    • 返回一个新的 连接套接字描述符(非负整数),专门用于与客户端通信。
    • 失败:返回 -1,并设置 errno。
      // 建立连接 (TCP, 客户端)

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

  • 参数:
    • socket:客户端套接字描述符(由 socket() 创建)
    • addr:指向目标服务器地址结构体(如 struct sockaddr_in)
    • addrlen:地址结构体的长度(如 sizeof(struct sockaddr_in))
  • 返回值:
    • 成功(TCP):返回 0,表示连接已建立。
    • 成功(UDP):仅绑定目标地址,无实际连接,后续 sendto() 可省略地址参数。
    • 失败:返回 -1,并设置 errno。

示例代码网址: Linux_blog: Linux博客示例代码 - Gitee.com

注意:

  • 由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。
  • 客户端不是不允许调用 bind(), 只是没有必要显示的调用 bind()固定一个端口号。否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接。
  • 服务器也不是必须调用 bind(), 但如果服务器不调用 bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦。

1.2、telnet小工具

telnet 是一个经典的 网络协议 和 命令行工具,用于通过 TCP/IP 网络 与远程主机进行交互式通信。它基于 Telnet 协议(默认端口 23),但也可以用于测试其他 TCP 服务(如 HTTP、SMTP 等)。

**基本用法:**telnet <主机名或IP> [端口]

示例:

  • telnet example.com # 连接 example.com 的 23 端口(Telnet 服务)
  • telnet 192.168.1.1 23 # 连接 192.168.1.1 的 23 端口

Telnet 不仅可以用于 Telnet 协议,还可以测试 HTTP、SMTP、SSH、Redis 等 TCP 服务:

  • telnet google.com 80 # 测试 HTTP(80 端口)
  • telnet smtp.gmail.com 25 # 测试 SMTP(25 端口)
  • telnet localhost 6379 # 测试 Redis(6379 端口)

手动发送 HTTP 请求:(按两次回车发送请求,查看服务器返回的 HTTP 响应)

telnet example.com 80

GET / HTTP/1.1

Host: example.com

常用操作:

  • Ctrl + ] 进入 Telnet 命令模式
  • quit 或 q 退出 Telnet
  • open <主机> <端口> 重新连接另一台主机
  • close 关闭当前连接
  • status 查看当前连接状态

二、验证 TCP-windows 作为 client 访问 Linux

示例代码:

TcpClient.cc:(Windows端)

cpp 复制代码
#include <winsock2.h>
#include <iostream>
#include <string>

#pragma warning(disable : 4996)

#pragma comment(lib, "ws2_32.lib")

std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号

int main()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0)
    {
        std::cerr << "WSAStartup failed: " << result << std::endl;
        return 1;
    }

    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET)
    {
        std::cerr << "socket failed" << std::endl;
        WSACleanup();
        return 1;
    }

    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口
    serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址

    result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
    if (result == SOCKET_ERROR)
    {
        std::cerr << "connect failed" << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }
    while (true)
    {
        std::string message;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        if(message.empty()) continue;
        send(clientSocket, message.c_str(), message.size(), 0);

        char buffer[1024] = {0};
        int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
        if (bytesReceived > 0)
        {
            buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
            std::cout << "Received from server: " << buffer << std::endl;
        }
        else
        {
            std::cerr << "recv failed" << std::endl;
        }
    }

    closesocket(clientSocket);
    WSACleanup();

    return 0;
}

TcpServer.cc:(Linux端)

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>

const static int default_backlog = 6;

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err,
    Listen_Err
};

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port), _isrunning(false)
    {
    }
    // 都是固定套路
    void Init()
    {
        // 1. 创建socket, file fd, 本质是文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            exit(0);
        }
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 2. 填充本地网络信息并bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);

        // 2.1 bind
        if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
        {
            exit(Bind_Err);
        }

        // 3. 设置socket为监听状态,tcp特有的
        if (listen(_listensock, default_backlog) != 0)
        {
            exit(Listen_Err);
        }
    }
    void ProcessConnection(int sockfd, struct sockaddr_in &peer)
    {
        uint16_t clientport = ntohs(peer.sin_port);
        std::string clientip = inet_ntoa(peer.sin_addr);
        std::string prefix = clientip + ":" + std::to_string(clientport);
        std::cout << "get a new connection, info is : " << prefix << std::endl;
        while (true)
        {
            char inbuffer[1024];
            ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);
            if(s > 0)
            {
                inbuffer[s] = 0;
                std::cout << prefix << "# " << inbuffer << std::endl;
                std::string echo = inbuffer;
                echo += "[tcp server echo message]";
                write(sockfd, echo.c_str(), echo.size());
            }
            else
            {
                std::cout << prefix << " client quit" << std::endl;
                break;
            }
        }
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 4. 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock, CONV(&peer), &len);
            if (sockfd < 0)
            {
                continue;
            }
            ProcessConnection(sockfd, peer);
        }
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensock; // TODO
    bool _isrunning;
};

using namespace std;

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

C++:

WinSock2.h 是 Windows Sockets API(应用程序接口)的头文件,用于在Windows 平台上进行网络编程。它包含了 Windows Sockets 2(Winsock2)所需 的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字 (sockets)进行网络通信。

在编写使用 Winsock2 的程序时,需要在源文件中包含 WinSock2.h 头文件。这 样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。

此外,与 WinSock2.h 头文件相对应的是 ws2_32.lib 库文件。在链接阶段,需要 将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2 API 中实现的函数。

在 WinSock2.h 中定义了一些重要的数据类型和函数,如:

  • WSADATA:保存初始化 Winsock 库时返回的信息。
  • SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。
  • sockaddr_in:IPv4 地址结构体,用于存储 IP 地址和端口号等信息。
  • socket():创建一个新的套接字。
  • bind():将套接字与本地地址绑定。
  • listen():将套接字设置为监听模式,等待客户端的连接请求。
  • accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信。
    C++:

WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock 库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首先执行,它扮演着初始化的角色。

以下是 WSAStartup 函数的一些关键点:

它接受两个参数:wVersionRequested 和 lpWSAData。wVersionRequested 用于指定所请求的 Winsock 版本,通常使用 MAKEWORD(major, minor)宏,其中major 和 minor 分别表示请求的主版本号和次版本号。lpWSAData 是一个指向WSADATA 结构的指针,用于接收初始化信息。

如果函数调用成功,它会返回 0;否则,返回错误代码。

WSAStartup 函数的主要作用是向操作系统说明我们将使用哪个版本的 Winsock库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock 库的状态会被初始化,应用程序就可以使用 Winsock 提供的一系列套接字 服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程 序可以与底层的网络协议栈进行交互,实现网络通信。

在调用 WSAStartup 函数后,如果应用程序完成了对请求的 Socket 库的使用,应 调用 WSACleanup 函数来解除与 Socket 库的绑定并释放所占用的系统资源。

三、connect 的断线重连

示例代码:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


using namespace std;

void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

enum class Status // C++11 强类型枚举
{
    NEW,        // 新建状态,就是单纯的连接
    CONNECTING, // 正在连接,仅仅方便查询conn状态
    CONNECTED,  // 连接或者重连成功
    DISCONNECTED, // 重连失败
    CLOSED        // 连接失败,经历重连,无法连接
};

class ClientConnection
{
public:
    ClientConnection(uint16_t serverport, const std::string &serverip)
        : _sockfd(-1),
          _serverport(serverport),
          _serverip(serverip),
          _retry_interval(1),
          _max_retries(5),
          _status(Status::NEW)
    {
    }
    void Connect()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            cerr << "socket error" << endl;
            exit(1);
        }

        // 2. 要不要bind?必须要有Ip和Port, 需要bind,但是不需要用户显示的bind,client系统随机端口
        // 发起连接的时候,client会被OS自动进行本地绑定
        // 2. connect
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        // p:process(进程), n(网络) -- 不太准确,但是好记忆
        inet_pton(AF_INET, _serverip.c_str(), &server.sin_addr); // 1. 字符串ip->4字节IP 2. 网络序列

        int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server)); // 自动进行bind哦!
        if (n < 0)
        {
            Disconnect();                   // 恢复_sockfd的默认值,是连接没有成功,不代表sockfd创建没有成功
            _status = Status::DISCONNECTED; // 没有连接成功
            return;
        }
        _status = Status::CONNECTED; // 连接成功
    }
    int SocketFd()
    {
        return _sockfd;
    }
    void Reconnect()
    {
        _status = Status::CONNECTING; // 正在重连
        int count = 0;
        while (count < _max_retries)
        {
            Connect(); // 重连
            if (_status == Status::CONNECTED)
            {
                return;
            }
            sleep(_retry_interval);
            count++;
            std::cout << "重连次数: " << count << ", 最大上限: " << _max_retries << std::endl;
        }
        _status = Status::CLOSED; // 重连失败,可以关闭了
    }
    void Disconnect()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
            _status = Status::CLOSED;
            _sockfd = -1;
        }
    }
    Status GetStatus()
    {
        return _status;
    }
    void Process()
    {
        // 简单的IO即可
        while (true)
        {
            string inbuffer;
            cout << "Please Enter# ";
            getline(cin, inbuffer);
            if(inbuffer.empty()) continue;
            
            ssize_t n = write(_sockfd, inbuffer.c_str(), inbuffer.size());
            if (n > 0)
            {
                char buffer[1024];
                ssize_t m = read(_sockfd, buffer, sizeof(buffer) - 1);
                if (m > 0)
                {
                    buffer[m] = 0;
                    cout << "echo messsge -> " << buffer << endl;
                }
                else if (m == 0) // 这里证明server端掉线了
                {
                    _status = Status::DISCONNECTED;
                    break;
                }
                else
                {
                    std::cout << "read m : " << m << "errno: " << errno << "errno string: " << strerror(errno) << std::endl;
                    _status = Status::CLOSED;
                    break;
                }
            }
            else
            {
                std::cout << "write n : " << n << "errno: " << errno << "errno string: " << strerror(errno) << std::endl;
                _status = Status::CLOSED;
                break;
            }
        }
    }
    ~ClientConnection()
    {
        Disconnect();
    }

private:
    int _sockfd;
    uint16_t _serverport;  // server port 端口号
    std::string _serverip; // server ip地址
    int _retry_interval;   // 重试时间间隔
    int _max_retries;      // 重试次数
    Status _status;        // 连接状态
};

class TcpClient
{
public:
    TcpClient(uint16_t serverport, const std::string &serverip) : _conn(serverport, serverip)
    {
    }
    void Execute()
    {
        while (true)
        {
            switch (_conn.GetStatus())
            {
            case Status::NEW:
                _conn.Connect();
                break;
            case Status::CONNECTED:
                std::cout << "连接成功, 开始进行通信." << std::endl;
                _conn.Process();
                break;
            case Status::DISCONNECTED:
                std::cout << "连接失败或者对方掉线,开始重连." << std::endl;
                _conn.Reconnect();
                break;
            case Status::CLOSED:
                _conn.Disconnect();
                std::cout << "重连失败, 退出." << std::endl;
                return; // 退出
            default:
                break;
            }
        }
    }
    ~TcpClient()
    {
    }

private:
    ClientConnection _conn; // 简单组合起来即可
};
// class Tcp

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);
    TcpClient client(serverport, serverip);
    client.Execute();
    return 0;
}