网络编程套接字(二)

简介: 网络编程套接字

简单的TCP网络程序


TCP与UDP的区别:是否需要链接,通信的数据类型也不同

TCP需要链接,通信数据是面向字节流


TCP通用服务端


tcp服务端初始化


与udp服务端不同的是,tcp服务需要先进行链接

举个栗子:

当你向客服进行询问,前提是客服随时都在线,即使没有客户进行询问时也必须在线,将这种状态称为“监听”;tcp服务端也是如此,在进行链接之前需要将socket进行监听(第二个参数后面会介绍)


int listen(int sockfd, int backlog);


// 3.设置socket为监听状态
if (listen(_listensock, gbacklog) < 0)
{
    std::cerr << "listen socket error" << std::endl;
    exit(LISTEN_ERR);
}
std::cout << "listen socket success!" << std::endl;


tcp服务端启动


接下来就是建立链接的过程,再举个栗子

在赌场周围会有拉客的人,称他们为张三;他们的目的就是拉客,将你拉入赌场进行消费,而当你进入赌场之后,真正为你服务的却不是那群拉客的人,而是里面的工作人员,称作李四;这里的张三的作用就只用来拉客,李四才是真的服务人员;tcp服务端也是如此,进行监听的socket在链接成功之后,会返回一个新的socket,新生成的socket的作用才是用来通信的;由于tcp通信是面向字节流,所以在链接成功之后,接下来的通信本质其实就是文件操作(IO操作)


int accept(int sockfd, struct sockaddr *addr, 
socklen_t *addrlen);


4008687f31805f3b2abb1069703546f6_c82caa2ae3b24cb999b10d86784a5800.png

// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
    std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl;
    continue;
}
std::cout << "accept success" << std::endl;


tcp服务端完整代码


namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };
    static const uint16_t gport = 8080;
    static const int gbacklog = 5;
    class TcpServer;
    class ThreadData
    {
    public:
        ThreadData(TcpServer *self, int sock)
            : _self(self), _sock(sock)
        {
        }
    public:
        TcpServer *_self;
        int _sock;
    };
    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port = gport)
            : _listensock(-1), _port(port)
        {
        }
        ~TcpServer()
        {
        }
        void InitServer()
        {
            // 1.创建socket文件套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                std::cerr << "create socket error" << errno << ":" << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "create socket success!" << std::endl;
            // 2.bind绑定自己的网络信息
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error" << errno << ":" << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket success!" << std::endl;
            // 3.设置socket为监听状态
            if (listen(_listensock, gbacklog) < 0)
            {
                std::cerr << "listen socket error" << std::endl;
                exit(LISTEN_ERR);
            }
            std::cout << "listen socket success!" << std::endl;
        }
        void start()
        {
            for (;;)
            {
                // 4.server获取新链接
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl;
                    continue;
                }
                std::cout << "accept success" << std::endl;
                // 多线程版
                pthread_t tid;
                ThreadData *td = new ThreadData(this, sock);
                pthread_create(&tid, nullptr, thread_routine, td);
            }
        }
        static void *thread_routine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadData *td = static_cast<ThreadData *>(args);
            td->_self->serviceIO(td->_sock);
            close(td->_sock);
            delete td;
            return nullptr;
        }
        void serviceIO(int sock)
        {
            char buffer[1024];
            while (true)
            {
                ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
                if (n > 0)
                {
                    // 目前为止将读到的数据当成字符串
                    buffer[n] = 0;
                    std::cout << "read message:" << buffer << std::endl;
                    std::string outbuffer = buffer;
                    outbuffer += "server[echo]";
                    write(sock, outbuffer.c_str(), outbuffer.size());
                }
                else if (n == 0)
                {
                    // 数据被读取完毕,客户端退出
                    std::cout << "client quit,me too!" << std::endl;
                    break;
                }
            }
            close(sock);
        }
    private:
        int _listensock;
        uint16_t _port;
    };
}


TCP通用客户端


tcp客户端初始化


tcp客户端初始化

这个过程与udp一样,不加赘述


void Initclient()
{
    // 1.创建socket
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0)
    {
        std::cerr << "socket create error" << errno << strerror(errno) << std::endl;
        exit(1);
    }
}


tcp客户端启动


既然服务端是监听,那么之后客服端发起链接之后,二者才能进行链接,然后通信


int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);


if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
    std::cerr << "socket connect error" << std::endl;
}
else
{
    std::string msg;
    while (true)
    {
        std::cout << "Enter#";
        std::getline(std::cin, msg);
        write(_sock, msg.c_str(), msg.size());
        char buffer[NUM];
        int n = read(_sock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            // 目前为止将读取的数据当成字符串
            buffer[n] = 0;
            std::cout << "Server回显#" << buffer << std::endl;
        }
        else
        {
            break;
        }
    }
}


tcp客户端完整代码


#define NUM 1024
namespace client
{
    class TcpClient
    {
    public:
        TcpClient(const std::string &serverip, const uint16_t &serverport)
            : _sock(-1), _serverip(serverip), _serverport(serverport)
        {
        }
        void Initclient()
        {
            // 1.创建socket
            _sock = socket(AF_INET, SOCK_STREAM, 0);
            if (_sock < 0)
            {
                std::cerr << "socket create error" << errno << strerror(errno) << std::endl;
                exit(1);
            }
        }
        void start()
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverport);
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
            {
                std::cerr << "socket connect error" << std::endl;
            }
            else
            {
                std::string msg;
                while (true)
                {
                    std::cout << "Enter#";
                    std::getline(std::cin, msg);
                    write(_sock, msg.c_str(), msg.size());
                    char buffer[NUM];
                    int n = read(_sock, buffer, sizeof(buffer) - 1);
                    if (n > 0)
                    {
                        // 目前为止将读取的数据当成字符串
                        buffer[n] = 0;
                        std::cout << "Server回显#" << buffer << std::endl;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
        ~TcpClient()
        {
            if (_sock >= 0)
                close(_sock);
        }
    private:
        int _sock;
        std::string _serverip;
        uint16_t _serverport;
    };
}


tcp通信展示


7c41c5aa9b92090baf211a4c9689eb16_1ec9de58a0ca49ed9440e7f3e2166786.png


这里还存在一个问题,服务器运行之后,之前的任何指令都会被当做消息被发送,不会再被命令行解释器执行;通过键盘可直接将其停止;真正的服务器应该是保存在云端,一直运行不会受其他因素影响


63cebba0580b8e9f7b8f2211f37b5248_23287ad59b9b4eec904b26507c2ceb7e.png


接下来接受守护进程来解决这个问题


守护进程


e55e794ff573d76d3d2bdefdd3fbfed5_55dfa5e7139d49279e3bb226e7b4ac2b.png


xshell链接远端服务器之后,服务器立刻形成一个会话:有且只有一个前台任务,多个后台任务;其中bash命令行解释器一般作为前台任务,用来执行各种指令


109a6ef40cc8d784ee43123b8bcb851d_865be761725347439143ec41d97dea33.png


通过命令行解释器形成两个后台任务;仔细观察会发现:两个任务分别是由两个组长(31271,31416)带领的,PGID为组长的代号;所有成员的共同领导是前台bash解释器(30990)


如果将其中一个后台任务,转换到前台结果会怎么样呢?


9c1f04d4f694eecf3df0a238edf2be64_99cd8c08b2e6485c84829fb7187bc0eb.png


由于Bash命令行解释器被切换为了后台,所以各种指令任务一都无法执行;和上面的情形一致,守护进程是自称会话,相当于自己既是领导也是组长同时也是员工


模拟实现守护进程

1.使调用进程忽略异常的信号


signal(SIGPIPE, SIG_IGN);

如何服务端出现异常,客服端向其发送消息时会直接忽略掉异常,可以继续发送


2.只有组员才能自成会话,setsid

举个栗子,在公司里面组长不允许直接离职创业,因为他需要管理下面的员工,但是员工就可以直接离职创业;这里采取的方式是,父进程退出,子进程自成会话


if (fork() > 0)
    exit(1);
// 子进程-》守护进程  本质也是孤儿进程的一种
pid_t id = setsid();
assert(id != -1);


3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件

进程默认会打开0,1,2文件描述符所指向的文件,为确保服务器不受器影响,需要将其关闭;这里采取的是将其重定向到”文件黑洞“,可以接受所有指令


d77fddc87807422916d118a5b311cf36_566749eb329949e39173ff6562fb2c2d.png


int fd = open(DEV, O_RDWR);
if (fd >= 0)
{
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
}
else
{
    close(0);
    close(1);
    close(2);
}


完整代码


#define DEV "/dev/null"
void deamonSelf()
{
    // 1.使调用进程忽略异常的信号
    signal(SIGPIPE, SIG_IGN);
    // 2.只有组员才能自成会话,setsid
    if (fork() > 0)
        exit(1);
    // 子进程-》守护进程  本质也是孤儿进程的一种
    pid_t id = setsid();
    assert(id != -1);
    // 3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件
    int fd = open(DEV, O_RDWR);
    if (fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }
}


42f0275f43bf15c22c597ab93c00f3c3_ce434dd5c3d7467bb13d55b85a3b94a7.png


服务端运行之后,确实立刻自成会话


25fc7a40051bd66fe44b5f8c2ae7d7c9_267ddf735f984487b9d38cb2277de3fe.png


查看服务端进程,被1号进程领养,自此网络通信告一段落


TCP协议通讯流程


b0f0bf24ac4f21f3d6e0850f9d3f1db3_9f9174aa32174acaac6e6447181eee62.png


服务器初始化:


调用socket, 创建文件描述符;

调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;

调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

调用accecpt, 并阻塞, 等待客户端连接过来;


建立连接的过程:


调用socket, 创建文件描述符;

调用connect, 向服务器发起连接请求;

connect会发出SYN段并阻塞等待服务器应答; (第一次)

服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)

客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)


这个建立连接的过程, 通常称为 三次握手;


数据传输的过程:


建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;

服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;

这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;

服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;

客户端收到后从read()返回, 发送下一条请求,如此循环下去;


断开连接的过程:


如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)

客户端收到FIN, 再返回一个ACK给服务器; (第四次)


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


目录
相关文章
|
2月前
|
网络协议 安全 网络安全
Qt 套接字类(QTcpSocket和QUdpSocket)解密:迈向 Qt 网络编程之巅
Qt 套接字类(QTcpSocket和QUdpSocket)解密:迈向 Qt 网络编程之巅
122 0
|
2月前
|
网络协议 Unix 开发者
套接字网络协议支持
套接字网络协议支持
10 1
|
2月前
|
网络协议 Java API
网络编程套接字(2)
网络编程套接字(2)
|
2月前
|
网络协议 Java
网络编程套接字(1)
网络编程套接字(1)
|
3月前
|
网络协议 Java
【JavaEE初阶】 网络编程基础与Socket套接字
【JavaEE初阶】 网络编程基础与Socket套接字
|
3月前
|
Java
[Java]Socket套接字(网络编程入门)
[Java]Socket套接字(网络编程入门)
38 0
|
4月前
|
网络协议 Java 程序员
网络编程套接字(Socket)
网络编程套接字(Socket)
37 0
|
4月前
|
监控 网络协议 Java
网络编程套接字
网络编程套接字
|
5月前
|
存储 网络协议 安全
网络编程『socket套接字 ‖ 简易UDP网络程序』
网络编程『socket套接字 ‖ 简易UDP网络程序』
78 0
|
6月前
|
存储 网络协议