Linux | 套接字(socket)编程 | TCP协议讲解 | 通信模型搭建

TCP模型的特性

TCP是属于传输层协议的一种,上篇博客介绍了另一种传输层协议——UDP,关于它们之间的区别,这里再提一下

TCPUDP
传输层协议传输层协议
有连接无连接
可靠连接不可靠连接
面向字节流面向数据报

其中更详细的区别在UDP套接字编程中已经讲解,这里不再赘述,直接进入TCP相关接口的介绍

TCP接口介绍

在这里插入图片描述
在这里插入图片描述
listen:使socket文件处于监听状态,此时网络中的设备可以向处于监听状态的套接字文件发送连接请求,backlog则是套接字文件的监听队列的长度。因为TCP协议是面向连接的协议,在通信之前需要双方确定连接,所以服务端通常需要先进入监听状态等待其他设备的连接,建立了连接才能进行通信。
在这里插入图片描述
accept:刚才的listen使一个socket文件处于监听状态,accept的作用就是接受socket的监听队列中的第一个套接字文件的连接,并且会创建一个服务套接字,服务套接字的套接字类型与协议家族和监听套接字的相同(端口号肯定不同),服务器用socket监听套接字监听网络中的连接请求,用accept返回的套接字文件为请求套接字提供服务。两个套接字文件都用来通信,一个是监听套接字,以通信的方式监听网络中的请求,一个是服务套接字,以通信的方式为发送请求的客户端提供服务。

参数解释:
socket:监听套接字的文件描述符
address:输出型参数,接受连接的套接字信息结构体
address_len:输出型参数,套接字信息的结构体的字节大小

所以accept可以接受监听队列中的连接请求,并以输出型参数的形式保存请求方的套接字信息,最后accept会返回一个套接字文件fd,用来为请求方提供服务。因此,只要accept了一次请求,就会有一个套接字文件被创建以提供服务,如果请求很多,用来提供服务的套接字相应的也会有很多,但是一台设备上的文件数是有上限的,如果对请求方提供的服务完成,没有及时关闭提供服务的套接字文件,就会造成文件资源的泄漏,最终造成服务器的崩溃,所以在编写TCP通信的代码时,一定要记住:当服务完成,用于提供服务的套接字文件必须及时关闭。
在这里插入图片描述
如果accept调用失败,函数会返回-1并设置错误码
在这里插入图片描述
补充两个IP地址转换的接口,一个是inet_aton,将字符串形式的IP转换成一个32位无符号整数

cp:是要转换字符串IP的起始地址
inp:是转换后的IP存储的地址

可以看到转换后的inp也是一个地址,并且其类型是struct in_addr*在这里插入图片描述
这类型不就对应了struct sockaddr_in的成员struct in_addr吗?原本需要直接对struct in_addr的成员in_addr_t类型的s_addr直接赋值,但是使用inet_aton函数就不需要了,该函数直接返回一个类型为struct in_addr的结构体,里面包含了以32位无符号整数存储的IP地址在这里插入图片描述

struct sockaddr_in local;
_ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);

(INADDR_ANY表示绑定该主机上的所有ip地址,只要网络中的设备发送的ip地址是该主机的其中一个,该主机就可以接受到)

还有一个接口inet_ntoa,可以将以32位整数形式表示的ip地址转换成点分十进制字符串形式的ip地址在这里插入图片描述
inet_ntoa的形参有点特殊,不是一个32位整数,而是一个结构体struct in_addr,该结构体是struct sockaddr_in结构体下的一个成员sin_addr的类型

struct sockaddr_in local;
char ip[] = inet_ntoa(local.sin_addr);
// ip数组就是以点分十进制表示的ip地址

在这里插入图片描述
connect:连接到一个socket文件,通常用于客户端与服务器的连接

socket:自己的socket文件fd,用自己的socket文件与服务器的socket文件进行通信
address:需要连接的套接字文件信息结构体
address_len:address结构体的字节大小

也就是说客户端使用connect与服务器连接,但是需要先知道服务器的套接字信息,创建了自己的套接字,有了服务器的套接字信息,就可以与服务器进行连接了

TCP服务器套接字设置

// util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>

#define SOCK_FAIL 1
#define BIND_FAIL 2
#define LSTE_FAIL 3
#define CONN_FAIL 4
#define USAG_ERRO 5
#include "util.hpp"
#include <signal.h>

class tcpServer
{
public:
    tcpServer(uint16_t port, std::string ip = "") : _ip(ip), _port(port) {}
    ~tcpServer() {}

    void init()
    {
        // 创建套接字文件
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "socket: fail" << std::endl;
            exit(SOCK_FAIL);
        }
        // 填充套接字信息
        struct sockaddr_in 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);
        // 将信息绑定到套接字文件中
        if (bind(_listen_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind: fail" << std::endl;
            exit(BIND_FAIL);
        }
        // 至此,套接字创建完成,所有的步骤与udp通信一样
        // 使套接字进入监听状态
        if (listen(_listen_sockfd, 5) < 0)
        {
            std::cerr << "listen: fail" << std::endl;
            exit(LSTE_FAIL);
        }
        // 套接字初始化完成
        std::cout << "listen done" << std::endl;
    }

    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // 设置SIGCHLD信号为忽略,子进程会自动释放资源
        // 创建保存套接字信息的结构体
        struct sockaddr_in peer;
        socklen_t peer_len = sizeof(peer);
        // 接受监听队列中的套接字请求
        while (1)
        {
            int server_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &peer_len);
            if (server_sockfd < 0)
            {
                std::cerr << "accept: fail" << std::endl;
                continue;
            }
            std::cout << "accept done" << std::endl;
            // 提取请求方的套接字信息
            uint16_t peer_port = ntohs(peer.sin_port);
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            // 打印请求方的套接字信息
            std::cout << "accept: " << peer_ip << " [" << peer_port << "]" << std::endl;
            // 提供服务
            pid_t id = fork();
            if (id == 0)
            {
                // child
                service(server_sockfd);
            }
        }
    }

    void service(int server_sockfd)
    {
        char in_buffer[1024];
        while (1)
        {
            ssize_t ret = read(server_sockfd, in_buffer, sizeof(in_buffer));
            if (ret > 0)
            {
                in_buffer[ret] = '\0';

                if (strcasecmp("quit", in_buffer) == 0)
                {
                    std::cout << "client quit, service done" << std::endl;
                    break;
                }
                // 假设服务是在client发送的字符串后添加一串字符串
                strcat(in_buffer, ",service done");
                // 服务完成,将字符串发送给client
                write(server_sockfd, in_buffer, sizeof(in_buffer));
            }
            else if (ret == 0)
            {
                std::cout << "client quit, service done" << std::endl;
                break;
            }
            else
            {
                std::cout << "service fail" << std::endl;
                break;
            }
        }
        close(server_sockfd);
    }

private:
    std::string _ip;
    uint16_t _port;
    int _listen_sockfd;
};

int main()
{
    tcpServer server(8080);
    server.init();
    server.loop();
    return 0;
}

封装tcpServer类,该类的成员_ip保存了服务器的IP地址,_port保存了端口号,以及_listen_sockfd保存了监听套接字的文件描述符。tcpServer的构造就是初始化这些成员,如果用户没有传入指定的IP地址,那么tcpServer的成员_ip就是一个空字符串,在绑定套接字信息时,会将该主机上的所有IP绑定到套接字文件中(INADDR_ANY)。

tcpServer的初始化函数:init,init会调用socket接口,创建一个套接字(打开一个套接字文件),然后填充套接字信息到struct sockaddr_in结构体中,接着调用bind绑定套接字信息到套接字文件中,至此TCP套接字的初始化和UDP通信一样,UDP通信的服务器此时已经初始化完成,可以调用recvfrom接收来自客户端的信息了。这是无连接的UDP通信,而TCP通信是面向连接的,所以在通信之前需要确定双方的连接,具体表现为:服务器调用listen进入监听状态,客户端调用connect连接处于监听状态的服务器。所以tcpServer的init初始化的套接字被用来监听网络中的连接请求,是客户端与服务器之间连接的窗口。因此,init总体分为四步,创建套接字,填充套接字信息,绑定套接字,使套接字处于监听状态,init完成,服务器就处于监听状态,可以接收网络中的连接请求。

服务器初始化后,就需要为客户提供服务,设置loop函数使服务器对外提供服务:先调用accept接收客户端向监听套接字发送的连接请求,由于accept会创建一个新的套接字并返回其文件描述符,所以我们需要接收accept的返回值,拿到新创建的套接字文件,用新的套接字文件为客户端提供服务。因此,在TCP模型中有两个套接字,一个监听套接字,一个服务套接字,监听套接字是连接服务器与客户端的窗口,服务套接字是服务器为客户端提供服务的窗口。只要服务器接受了监听套接字监听到的连接请求,服务器就需要创建一个服务套接字,通过服务套接字为客户端提供服务。但是服务器是一个进程,所以服务器提供服务时只能一对一的提供,服务完成一个客户再服务下一个客户,很显然,这样一个一个的服务效率太低,所以我们可以创建子进程,使子进程通过服务套接字为客户端提供服务。

但是当子进程服务完成,子进程就会退出,此时的子进程处于僵尸状态,父进程需要回收子进程的资源,如果不回收子进程就会引起资源泄漏的问题。所以父进程需要调用waitpid回收子进程,但是父进程又不能阻塞式的等待子进程的退出,阻塞式的等待与服务器一个个的服务客户没有区别,所以父进程就需要非阻塞式的等待子进程,但是父进程就需要记录所有子进程的pid,这样也有点麻烦,最简单的方法就是将SIGCHLD信号的handler设置为SIG_IGN,使子进程在退出时自动释放自己的资源(但这种方法只有在Linux下有效)。

除了创建子进程,使子进程为客户提供服务的做法,我们还可以创建孙子进程,使父进程退出。就是说,现在的服务器是一个祖父进程,服务器创建了一个子进程,接着在这个子进程中再创建一个子进程,那么服务就创建了两个子进程,它们的关系是祖父进程,父进程与子进程,我们可以使父进程退出,那么子进程成为孤儿进程,由1号进程托管,对客户端提供的服务由子进程完成,当子进程退出时,1号进程就会自动释放它的资源,这样它就不会处于僵尸状态,造成资源的泄漏了。但是祖父进程就需要阻塞式的等待回收父进程(当父进程创建完子进程后,父进程就会退出,可以说一瞬间父进程就退出了,祖父进程也不会等待太久),不然父进程就会进入僵尸,又造成资源泄漏。
在这里插入图片描述
除了创建孙子进程这样的操作,我们还能创建多线程,但是我们不能join新线程,因为join会造成进程的阻塞,我们只能将线程分离,使其退出时自动释放资源
在这里插入图片描述

在这里插入图片描述

TCP客户端套接字设置

#include "util.hpp"

void usage(const char *filename)
{
    std::cout << "usage:\n\t"
              << filename << "IP port" << std::endl;
}

volatile bool quit = false;

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAG_ERRO);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 服务器套接字的填充
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 套接字的创建
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket: fail" << std::endl;
        exit(SOCK_FAIL);
    }
    // 与服务器的连接
    if (connect(sockfd, (const struct sockaddr*)&server, sizeof(server)) < 0)
    {
        std::cerr << "connect: fail" << std::endl;
        exit(CONN_FAIL);
    }
    std::cout << "connect done" << std::endl;
    while (!quit)
    {
        std::cout << "Please Enter#";
        char in_buffer[1024] = {0};
        char out_buffer[1024] = {0};
        std::cin.getline(in_buffer, sizeof(in_buffer));
       // std::cout << in_buffer << std::endl;
        if (strcasecmp("quit", in_buffer) == 0)
        {
            // 注意不要退出,让客户端向服务器发送quit,服务器接收quit将关闭服务
            quit = true;
        }

        ssize_t w_ret = write(sockfd, in_buffer, sizeof(in_buffer));
        if (w_ret > 0)
        {
            ssize_t r_ret = read(sockfd, out_buffer, sizeof(out_buffer));
            if (r_ret > 0)
            {
                out_buffer[r_ret] = '\0';
                std::cout << "receive: " << out_buffer << std::endl;
            }
            else 
            {
                std::cerr << "read: fail" << std::endl;
                break;
            }
        }
        else
        {
            std::cerr << "write: fail" << std::endl;
            break;
        }
    }
    return 0;
}

客户端要做的就是:填充服务器的套接字信息,并创建自己的套接字,最重要的就是调用connect用自己的套接字连接服务器的监听套接字,也就是发送与服务器的连接请求,当服务器调用listen处于监听状态时,客户端的连接才能发送成功,由于TCP协议下发送的数据是流式的,所以我们可以使用write和read向套接字文件中读写信息(UDP的数据是数据报式的,recvfrom和sendto是用来发送数据报式数据的接口)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值