深入了解linux网络—— TCP网络通信(上)

前言

了解了UDP通信相关接口,现在来学习TCP通信的相关接口

服务端

无论是UDP通信,还是TCP通信;都要创建套接字、绑定端口号。

1. 初始化

创建套接字

c 复制代码
int socket(int domain, int type, int protocol);

这里要使用TCP通信,传递的参数就应该是:AF_INETSOCKSTREAM(面向字节流)

c 复制代码
socket(AF_INET,SOCK_STREAM,0); //AF_INET 网络通信   SOCK_STREAM  面向字节流

对于socket返回值,是一个文件描述符;和UDP使用略有差别(在accept详细介绍)

绑定端口号

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

对于bind绑定端口号,需要sockfdstruct sockaddr*类型的指针对象;(这里就直接使用封装好的InetAddr具体是实现在:lesson17/chat/inetaddr.hpp · 迟来的grown/linux

c 复制代码
bind(_sockfd, addr.GetInetAddr(), addr.GetLen());

这里服务端,IP地址就直接绑定INADDR_ANY

监听状态

对于UDP通信,只需要创建套接字、绑定端口号就可以了;

TCP通信,除此之外还需要设置监听状态

设置监听状态要是有接口 : listen

c 复制代码
int listen(int sockfd, int backlog);

参数:

  • sockfd:创建套接字socket返回的文件描述符。
  • backlog:表示该套接字维护的连接请求队列的最大长度,也就是:等待被接受的最大连接数

返回值:

成功返回0、失败则返回-1

所以,对于UDP通信,只需要创建套接字和绑定端口号;

TCP通信,还需要设置监听状态。

cpp 复制代码
class TcpServer
{
public:
    TcpServer(uint16_t port) : _sockfd(-1), _port(port)
    {
    }
    ~TcpServer() {}
    void Init()
    {
        // 1. socket
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(Level::FATAL) << "socket error";
            exit(1);
        }
        LOG(Level::DEBUG) << "socket success";
        // 2. bind
        InetAddr addr(_port);
        int b = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());
        if (b < 0)
        {
            LOG(Level::FATAL) << "bind error";
            exit(2);
        }
        LOG(Level::DEBUG) << "bind success";
        // 3. listen
        int l = listen(_sockfd, 5);
        if (l < 0)
        {
            LOG(Level::FATAL) << "listen error";
            exit(3);
        }
        LOG(Level::DEBUG) << "listen success";
    }
private:
    int _sockfd;
    uint16_t _port;
};

这样,在使用时,只需创建TcpServer对象,直接调用即可:

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "usage : " << argv[0] << " port" << std::endl;
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    TcpServer tsvr(port);
    tsvr.Init();
    sleep(100);
    return 0;
}

这里,我们可以使用netstat命令查看:(ntestat -naltp)

-l: 监听状态; -t:TCP通信


2. 读取消息

UDP通信中,创建套接字、绑定端口号之后,就可以直接调用sendtorecvfrom进行发送和接受信息。

而在TCP中,进行读取消息之前,还需要建立连接;(服务端获取连接请求,客户端发送连接请求)。

只有建立了连接,才能进行网络通信。

获取连接

客户端获取连接请求所有到的接口:accept

c 复制代码
       int accept(int sockfd, struct sockaddr *_Nullable restrict addr,
                  socklen_t *_Nullable restrict addrlen);

参数

参数相对来说还是非常好理解的:

  • sockfd创建套接字所返回的文件描述符;
  • addr:输出型参数,获取远端的addr
  • addrlen传参时表示addr的长度,调用成功后表示所获取到远端addr的长度。

返回值:

对于accept的返回值就非常有意思了:

看看到,如果调用成功,返回一个文件描述符;那accept返回的文件描述符和socket返回的文件描述符有什么区别呢?

socket:创建套接字返回的文件描述符,该文件描述符只用来绑定端口号和获取连接请求。

accept:对于accept返回的文件描述符,在通信时读取使用。

简单来说就是,一个服务端可能连接多个客户端;

每一个连接都存在一个文件描述符,在服务的通过文件描述符来 接受/发送信息 给客户端。

接受/发送信息

对于TCP通信,要接受信息用的接口是read;(就是进行文件读操作的read)

而发送信息用的接口是write;(就是文件写操作的write

要读取信息,用的就是accept返回的文件描述符

cpp 复制代码
    void Server(int rwfd)
    {
        while (true)
        {
            char buff[256];
            int rn = read(rwfd, buff, sizeof(buff) - 1);
            if (rn < 0)
            {
                // read出错
                LOG(Level::ERROR) << "read error";
                break;
            }
            else if (rn == 0)
            {
                // write端退出
                LOG(Level::INFO) << "writer is exit";
                break;
            }
            // 读取成功
            buff[rn] = '\0';
            std::cout << "read : " << buff << std::endl;
            // 发送信息, 这里简单将信息发送回去
            int wn = write(rwfd, buff, rn);
            if (wn < 0)
            {
                LOG(Level::ERROR) << "write error";
                break;
            }
        }
    }
    void Start()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            bzero(&peer, len);
            int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (rwfd < 0)
            {
                LOG(Level::FATAL) << "accept error";
                exit(4);
            }
            LOG(Level::DEBUG) << "accept success";
            // 读写
            Server(rwfd);
        }
    }

这里读写操作和文件读写一模一样。

简单来说:TCP通信,socket返回的文件描述符只用来绑定bind、监听listen和获取连接请求accept使用;

而进行通信使用的都是accept返回的文件描述符。

3. telnet 测试

这里只是实现了server端代码,简单测试一下;

telnet命令可以用来连接某IP地址端口号(发送连接请求)

复制代码
telnet IP port

这里可以使用netstat -natlp查看连接情况:

这里是在一台服务器上做测试,可以看到两条连接。

客户端

对于客户端,首先还是要创建套接字;

还是无需显示绑定UDP通信时,是首次发送信息时绑定;那TCP呢?)

TCP通信中,则是在connect成功时自动绑定。

所以,客户端除了创建套接字之外,还需要做的就是发送连接请求;

cpp 复制代码
       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

对于connect的参数,还是非常容易理解的:

  • sockfd:创建套接字返回的文件描述符。
  • addr:远端的sockaddr_in字段。
  • addrlenaddr的长度

返回值:


客户端绑定是在connect成功时自动绑定的;

connect成功/绑定成功,返回0;否则返回-1,且错误码被设置。

读写操作

对于客户端读写操作,还是使用readwrite接口;

所用的文件描述符就是socket返回的文件描述符。

cpp 复制代码
#include "log.hpp"
#include "inetaddr.hpp"
using namespace hllog;
using namespace hladdr;
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "usage : " << argv[0] << " server_ip server_port" << std::endl;
        exit(1);
    }
    InetAddr server(argv[1], std::stoi(argv[2]));
    // 1. socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        LOG(Level::FATAL) << "socket error";
        exit(1);
    }
    LOG(Level::INFO) << "socket success, sockfd : " << sockfd;

    // 2. connect
    int n = connect(sockfd, server.GetInetAddr(), server.GetLen());
    if (n < 0)
    {
        LOG(Level::FATAL) << "connect error";
        exit(2);
    }
    LOG(Level::INFO) << "connect success, sockfd : " << sockfd;

    // 写/读
    while (true)
    {
        std::string massage;
        std::cout << "Please Enter #";
        std::getline(std::cin, massage);
        int wn = write(sockfd, massage.c_str(), massage.size());
        if (wn < 0)
        {
            LOG(Level::WARNING) << "write error";
            break;
        }
        // 接受
        char buff[256];
        int rn = read(sockfd, buff, sizeof(buff) - 1);
        if (rn < 0)
            continue;
        else if (rn == 0)
            break;
        buff[rn] = '\0';
        std::cout << "recive : " << buff << std::endl;
    }
    return 0;
}

多进程

对于上述实现的代码,存在一个bug:一次只能处理一个client端的请求;

这是因为在server获取到一个连接时,就会长服务式的处理这个请求(读写);只要这个连接不退出,server就无法获取新的连接请求。

这里就将上述代码修改成多进程的:

server端获取到一个连接时,就创建一个子进程,让子进程去服务;父进程继续获取请求。

问题:子进程退出时,父进程如何回收?何时回收?

  • 解决方案1:将tcpserver进程对SIGCHLD信号的处理方式设置成SIG_IGN或者自定义捕捉。
  • 解决方案2 :子进程再创建子进程(孙子进程),然后子进程退出,tcpserver回收子进程;孙子进程去服务(孤儿进程,进程推了操作系统自动回收)。

对于多进程要注意:在创建子进程后要关闭不用的文件描述符。

这里就直接实现方案二:

cpp 复制代码
    void Server(int rwfd)
    {
        while (true)
        {
            char buff[256];
            int rn = read(rwfd, buff, sizeof(buff) - 1);
            if (rn < 0)
            {
                // read出错
                LOG(Level::ERROR) << "read error";
                break;
            }
            else if (rn == 0)
            {
                // write端退出
                LOG(Level::INFO) << "writer is exit";
                break;
            }
            // 读取成功
            buff[rn] = '\0';
            std::cout << "read : " << buff << std::endl;
            // 发送信息, 这里简单将信息发送回去
            int wn = write(rwfd, buff, rn);
            if (wn < 0)
            {
                LOG(Level::ERROR) << "write error";
                break;
            }
        }
    }
    void Start()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            bzero(&peer, len);
            int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (rwfd < 0)
            {
                LOG(Level::FATAL) << "accept error";
                exit(4);
            }
            LOG(Level::DEBUG) << "accept success";
            // 读写
            // Server(rwfd);

            // 多进程
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(Level::FATAL) << "fork error";
                exit(1);
            }
            else if (pid == 0)
            {
                close(_sockfd);
                if (fork() == 0)
                    Server(rwfd);
                exit(0);
            }
            // 父进程
            waitpid(pid, nullptr, 0);
        }
    }

多线程

要实现多线程版本,创建线程去执行Server

但是Server的是void(TcpServer*, int)类型的,创建线程要执行的方法:void*(void*)类型。

并且,创建出来的线程是不知道通信要使用的文件描述符的。

  • 对于线程无法访问到通信要使用的文件描述符,这里直接在TcpServer中使用一个类,表示线程调用Server需要的数据。

    需要哪些数据呢?(fd读写使用的文件描述符、TcpServer*类型的指针对象用来调用Server方法,要知道远端通信对方的IP地址和port,也需要InetAddr类型对象。)

  • 对于线程执行方法,使用一个静态成员方法Routinue,通过传参将所需要的数据传递进去。

在创建完线程之后,设置新线程detach分离,无需手动回收

cpp 复制代码
        void Server(int rwfd, InetAddr &addr)
    	{
            while (true)
            {
                char buff[256];
                int rn = read(rwfd, buff, sizeof(buff) - 1);
                if (rn < 0)
                {
                    // read出错
                    LOG(Level::ERROR) << "read error";
                    break;
                }
                else if (rn == 0)
                {
                    // write端退出
                    LOG(Level::INFO) << "writer is exit";
                    break;
                }
                // 读取成功
                buff[rn] = '\0';
                std::cout << addr.ToString() << " : " << buff << std::endl;
                // 发送信息, 这里简单将信息发送回去
                int wn = write(rwfd, buff, rn);
                if (wn < 0)
                {
                    LOG(Level::ERROR) << "write error";
                    break;
                }
            }
        }
   class ThreadData
    {
    public:
        ThreadData(int fd, TcpServer *tsvr, InetAddr addr)
            : _fd(fd), _tsvr(tsvr), _addr(addr)
        {
        }
        int _fd;
        TcpServer *_tsvr;
        InetAddr _addr;
    };
    static void *Routinue(void *argv)
    {
        ThreadData *td = static_cast<ThreadData *>(argv);
        td->_tsvr->Server(td->_fd, td->_addr);
        return nullptr;
    }
    void Start()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            bzero(&peer, len);
            int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (rwfd < 0)
            {
                LOG(Level::FATAL) << "accept error";
                exit(4);
            }
            LOG(Level::DEBUG) << "accept success";
            // 多线程
            pthread_t tid;
            ThreadData* td = new ThreadData(rwfd,this,peer);
            pthread_create(&tid,nullptr,Routinue,td);
        }
    }

到这里,本篇文章内容就结束了,感谢支持

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
青草地溪水旁3 小时前
socketpair深度解析:Linux中的“对讲机“创建器
linux·服务器·socket编程
爱尚你19933 小时前
Nginx proxy_pass 末尾斜杠(/)
服务器·网络·nginx
想唱rap3 小时前
Linux指令(1)
linux·运维·服务器·笔记·新浪微博
woshihonghonga3 小时前
Ubuntu20.04下的Pytorch2.7.1安装
linux·人工智能·ubuntu
字节高级特工3 小时前
网络协议分层与Socket编程详解
linux·服务器·开发语言·网络·c++·人工智能·php
minji...5 小时前
Linux 权限的概念及shell命令运行原理
linux·运维·服务器
light_in_hand5 小时前
NAT 机制的工作流程
网络
欢鸽儿5 小时前
理解Vivado的IP综合策略:“Out-of-Context Module Runs
linux·ubuntu·fpga
Pan Zonghui5 小时前
腾讯云COS通过CDN加速配置指南
网络·云计算·腾讯云