Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux之TCP网络套接字

Linux之TCP网络套接字详解

作者:每天敲200行代码

TCP socket API包含创建、绑定、监听、接受连接、连接等步骤,需三次握手建立连接,四次挥手断开,与UDP相比,TCP是面向连接的可靠传输协议,基于字节流进行数据交换,确保双向通信可靠性

一、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 对比

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文