服务器 > 服务器 > Linux

Linux之TCP网络套接字详解

32人参与 2025-08-11 Linux

一、tcp socket api 详解

1、插件套接字 socket

在通信之前要先把网卡文件打开。

成功则返回打开的文件描述符(指向网卡文件),失败返回-1。 

这个函数的作用是打开一个文件,把文件和网卡关联起来。 

(只需要关注上面两个类,第一个 af_unix 表示本地通信,而 af_inet 表示网络通信。

从这里我们就可以联想到系统中的文件操作,未来各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。

2、绑定 bind

 

bind() 成功返回 0,失败返回 -1。

所以我们需要先定义一个address_in 结构体填充数据,再传递进去。

3、设置监听状态 listen

因为 tcp 是面向连接的,当我们正式通信的时候,需要先建立连接,那么 tcp 跟 udp 的不同在这里就体现了出来。要把 socket 套接字的状态设置为 listen 状态,只有这样才能一直获取新链接,接收新的链接请求。

举例帮助理解:我们买东西如果出现了问题会去找客服,如果客服不在,那么就无法回复我们,所以就规定了客服在工作的时候必须要时刻接收回复消息,那么这个客服所处的状态就叫做监听状态。

 

关于第二个参数:backlog,后边讲 tcp 协议参数时会再详细介绍,目前先直接用。( 一般不能太大,也不能太小)

listen() 成功返回 0,失败返回 -1。 

创建套接字成功,套接字对应的文件描述符值是 3,为什么是 3 呢?

因为当前对应的文件描述符返回的套接字本身就是一个文件描述符,0、1、2 被占用,再创建一个文件,对应的就是 3。

4、获取新链接 accept

前面初始化完成,现在就是要开始运行服务端。tcp 不能直接发送数据,因为它是面向链接的,所以必须要先建立链接。

成功返回一个文件描述符,失败返回 -1。

三次握手完成后,服务器调用 accept() 接受连接。

如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

addr 是一个传出参数,accept() 返回时传出客户端的地址和端口号。

如果给 addr 参数传 null,表示不关心客户端的地址。

addrlen 参数是一个传入传出参数 (value-result argument),传入的是调用者提供的,缓冲区 addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

我们的服务器程序结构是这样的:

sockfd 本来就是一个文件描述符,那么这个返回的文件描述符是什么呢?

那么我们就知道了,成员变量中的 _sock 并不是通信用的套接字,而是获取链接的套接字。为了方便观察,我们可以把前面所有的 _sock 换成 _listensock。

客服端的整体代码如下:

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

log lg;
class tcpserver
{
public:
    // 下面这个构造函数时全缺省的,就不需要这个默认构造了
    // tcpserver()
    // {
    // }
    // 第一个参数时listensock,但是这个时在init的时候socket接口创建的套接字,你如果传参的话,第一个传递的应该时监听套接字,而不是一个端口号,
    // 你在调用的时候,传递的时一个端口号,所以这里时有问题的,
    // tcpserver(int sock = 0, uint16_t port = 8080, std::string ip = "0.0.0.0")
    //     : listensock_(sock), port_(port), ip_(ip)
    // {
    // }
    tcpserver(uint16_t port = 8080, std::string ip = "0.0.0.0")
        : listensock_(-1), port_(port), ip_(ip)
    {
    }
    void init()
    {
        listensock_ = socket(af_inet, sock_stream, 0);
        if (listensock_ < 0)
        {
            lg(fatal, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        lg(info, "create socket success,sockfd:%d", listensock_);
        struct sockaddr_in local;
        bzero(&local, 0);
        local.sin_family = af_inet;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        socklen_t len = sizeof(local);
        if (bind(listensock_, (const sockaddr *)&local, len) < 0)
        {
            lg(fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(3);
        }
        lg(info, "bind success!");
        if (listen(listensock_, 5) < 0)
        {
            lg(fatal, "listen error, errno: %d, err string: %s", errno, strerror(errno));
            exit(4);
        }
    }
    void start()
    {
        lg(info, "tcpserver is running....");
        // 1. 获取新连接
        char buffer[1024];
        struct sockaddr_in client;
        memset(&client, 0, sizeof(client));
        socklen_t len = sizeof(client);

        while (true)
        {
            // 这里是由问题的,因为你获取一个连接之后,应该是多次进行通信,而不是通信一次之后,在获取连接,因为上一个连接还没有处理完呢
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            string clientip = inet_ntoa(client.sin_addr);
            // 连接成功后,为客户端提供服务
            while (1)
            {
                // 连接成功之后,多次处理这个连接的请求
                ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);
                if (size > 0)
                {
                    buffer[size] = 0;
                    cout << '[' << clientip << ':' << clientport << "]# " << buffer << endl;
                    write(sockfd, buffer, size);
                }
                else if (size == 0)
                {
                    // 如果读取到0的时候,就是对方断开了,
                    // 需要做的事情就是关闭套接字,然后跳出循环,获取下一个连接
                    close(sockfd);
                    break;
                }
                else
                {
                    // 如果是小于0的话,那么就说明read这个错误失败了,也需要关闭连接
                    close(sockfd);
                    break;
                }
            }
        }
        // while (true)
        // {
        //     // 这里是由问题的,因为你获取一个连接之后,应该是多次进行通信,而不是通信一次之后,在获取连接,因为上一个连接还没有处理完呢
        //     int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        //     if (sockfd < 0)
        //     {
        //         lg(warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
        //         continue;
        //     }
        //     uint16_t clientport = ntohs(client.sin_port);
        //     string clientip = inet_ntoa(client.sin_addr);
        //     // 连接成功后,为客户端提供服务
        //     ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);
        //     if (size > 0)
        //     {
        //         buffer[size] = 0;
        //         cout << '[' << clientip << ':' << clientport << "]# " << buffer << endl;
        //         write(sockfd, buffer, size);
        //         std::cout << 1 <<std::endl;
        //     }
        // }
    }

public:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

 

5、发起链接 connect

 

connect() 成功返回 0,出错返回 -1。

这里的 addr 和 addrlen 填入的是服务端信息。 

在 udp 通信中,客户端在 sendto 时会自动绑定 ip 和 port,而 tcp 就是在 connect 的时候进行绑定。因为 connect 是系统调用接口,所以在调用 connect 时会自动的给绑定当前客户端的 ip 和 port,进而可以让我们在后续使用 sockfd 进行通信。

客户端.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
using std::cout;
using std::endl;

void usage(const std::string &proc)
{
    std::cout << "\n\rusage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = af_inet;
    server.sin_port = htons(serverport);
    inet_pton(af_inet, serverip.c_str(), &(server.sin_addr));
    int sockfd = socket(af_inet, sock_stream, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 1;
    }
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error..." << std::endl;
        exit(2);
    }
    cout << "connect sunccess!" << endl;
    while (true)
    {
        while (true)
        {
            std::string message;
            std::cout << "please enter# ";
            std::getline(std::cin, message);

            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                std::cerr << "write error..." << std::endl;
                // break;
            }

            char inbuffer[4096];
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << inbuffer << std::endl;
            }
            else
            {
                // break;
            }
        }

        close(sockfd);
    }

    return 0;
}

运行结果如下:

二、tcp 协议通讯流程

下图是基于 tcp 协议的客户端/服务器程序的一般流程:

1、服务器初始化

2、建立连接的过程

tcp 是面向连接的通信协议,在通信之前需要进行 3 次握手,来进行连接的建立。这个建立连接的过程通常称为 三次握手 

3、数据传输的过程

4、断开连接的过程

当 tcp 断开连接这个断开连接的过程 , 通常称为 四次挥手 。

为什么是四次挥手呢?

三、总结

对比 udp 服务器,tcp 服务器多了获取新链接和监听的操作,而因为 tcp 是面向字节流的,所以接收和发送数据都是 io 操作,也就是文件操作。

tcp 和 udp 对比

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

Linux云服务器开放端口详解

08-11

Linux部署中的文件大小写问题的解决方案

08-11

Linux UDP网络编程套接字sockets介绍

08-11

Linux进程信号的捕捉&&信号补充内容方式

08-11

Linux查询服务器系统版本号的多种方法

08-12

linux如何查看kafka的消费组里是否有积压

08-08

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论