【网络】套接字 -- TCP

作者华丞臧.
​​​​专栏:【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 LeetCode刷题网站


文章目录

  • 一、TCP协议
    • 1.1 socket 常见API
      • listen
      • accept
      • connect
    • 1.2 TCP服务器
      • init()
      • start()
    • 1.3 TCP客户端
    • 1.4 测试


一、TCP协议

对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识:

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

1.1 socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果socket()调用出错则返回-1;
  • 对于IPv4, family参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可。
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听
  • myaddr所描述的地址和端口号;
  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

程序中对 sockaddr 参数是这样初始化的:

#include 
#include 

// 2.填充域
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
  4. 端口号为SERV_PORT, 我们定义为9999。

listen

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

accept

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度
  • 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

connect

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

1.2 TCP服务器

【网络】套接字 -- TCP_第1张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "Log.hpp"

volatile bool quitSer = false;

void Usage(void *vgs)
{
    std::cout << "Usage:./tcpserver port ip" << std::endl;
}
class server
{
public:
    server(int port, std::string ip = "")
        : sockfd_(-1)
        , ip_(ip)
        , port_(port)
    {
    }
    ~server()
    {
    }

public:
    void init()
    {}
    void start()
    {}

private:
    int sockfd_;
    uint16_t port_;
    std::string ip_;
};

// ./tcpserver port ip
int main(int argc, char *argv[])
{
    if (argc < 2 || argc > 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
        ip = argv[2];

    server tcpSer(port, ip);
    tcpSer.init();
    tcpSer.start();
    return 0;
}

init()

TCP套接字服务器端的 init() ,与UDP类似,只不过多了最后一步监听套接字,TCP套接字设置步骤如下:

【网络】套接字 -- TCP_第2张图片

void init()
{
    // 1. 创建套接字
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd_ < 0)
    {
        logMessage(FATAL, "socket:%s[%d]", strerror(errno), sockfd_);
        exit(SOCK_ERR);
    }
    logMessage(DEBUG, "socket success..");

    // 2.填充域
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port_);
    ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));

    // 3. 绑定网络信息
    if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "bind:%s[%d]", strerror(errno), sockfd_);
        exit(BIND_ERR);
    }
    logMessage(DEBUG, "bind success...");

    // 4. 监听套接字
    // 为什么要监听套接字? 因为TCP是面向连接的,在任何时候都可能请求连接 
    if (listen(sockfd_, 5) < 0)
    {
        logMessage(FATAL, "listen:%s[%d]", strerror(errno), sockfd_);
        exit(LISTEN_ERR);
    }
    logMessage(DEBUG, "listen success...");

    // 完成
}

start()

在TCP套接字提供服务之前,需要使用accept获取连接,accept返回值是一个新的套接字文件描述符,使用该套接字文件描述符来进行网络通信。start()是server提供服务的接口,因此该函数必须是一个死循环(服务器都是在一个死循环当中)以给用户提供持续的服务;在start函数中,主要完成接收用户发送的消息并且将消息提取出来,其主要步骤如下图:
【网络】套接字 -- TCP_第3张图片

void start()
{
    char inbuffer_[1024]; // 用来接收客户端发来的消息
    // 提供服务
    while (true)
    {
        quitSer = false;
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 5. 获取连接, accept 的返回值是一个新的 socketfd
        int serviceSock = accept(sockfd_, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
            continue;
        }

        while (!quitSer)
        {
            memset(inbuffer_, 0, sizeof(inbuffer_));
            ssize_t s = recvfrom(serviceSock, inbuffer_, sizeof(inbuffer_) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                std::cout << s << std::endl;
                // 接收成功
                inbuffer_[s] = '\0';
            }
            else if (s == -1)
            {
                //
                logMessage(WARINING, "recvfrom fialed:%s[%d]", strerror(errno), sockfd_);
                continue;
            }
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
            if(s > 0)
                logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer_);
            if (strcmp(inbuffer_, "quit") == 0)
            {
                quitSer = true;
            }
            else
            {
                sendto(serviceSock, inbuffer_, strlen(inbuffer_), 0,\
                    (const struct sockaddr *)&peer, sizeof(peer)); 
            }
        }
        
        logMessage(DEBUG, "quit server...");
        close(serviceSock);
    }
}

1.3 TCP客户端

TCP客户端与UDP不同的是,在进行网络通信之前需要对服务器发起连接请求,连接成功后才能进行网络通信。
【网络】套接字 -- TCP_第4张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

volatile bool quit = false;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
    std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
              << std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);

    // 1. 创建socket SOCK_STREAM
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket: " << strerror(errno) << std::endl;
        exit(SOCK_ERR);
    }

    // 2. connect,发起连接请求,你想谁发起请求呢??当然是向服务器发起请求喽
    // 2.1 先填充需要连接的远端主机的基本信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_aton(serverIp.c_str(), &server.sin_addr);
    // 2.2 发起请求,connect 会自动帮我们进行bind!
    if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        std::cerr << "connect: " << strerror(errno) << std::endl;
        exit(CONN_ERR);
    }
    std::cout << "info : connect success: " << sock << std::endl;

    std::string message;
    while (!quit)
    {
        message.clear();
        std::cout << "请输入你的消息>>> ";
        std::getline(std::cin, message);
        if (strcasecmp(message.c_str(), "quit") == 0)
            quit = true;

        ssize_t s = write(sock, message.c_str(), message.size());
        logMessage(DEBUG, "write success...");
        if (s > 0)
        {
            message.resize(1024);
            ssize_t s = read(sock, (char *)(message.c_str()), 1024);
            if (s > 0)
                message[s] = 0;
            std::cout << "Server Echo>>> " << message << std::endl;
        }
        else if (s <= 0)
        {
            break;
        }
    }
    close(sock);
    return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。

注意:

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

1.4 测试

【网络】套接字 -- TCP_第5张图片

你可能感兴趣的:(网络,网络,tcp/ip,网络协议)