网络编程套接字(二)

简介: 网络编程套接字

简单的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月前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
23 1
|
3月前
|
网络协议 算法 网络性能优化
C语言 网络编程(十五)套接字选项设置
`setsockopt()`函数用于设置套接字选项,如重复使用地址(`SO_REUSEADDR`)、端口(`SO_REUSEPORT`)及超时时间(`SO_RCVTIMEO`)。其参数包括套接字描述符、协议级别、选项名称、选项值及其长度。成功返回0,失败返回-1并设置`errno`。示例展示了如何创建TCP服务器并设置相关选项。配套的`getsockopt()`函数用于获取这些选项的值。
|
3月前
|
网络协议
关于套接字socket的网络通信。&聊天系统 聊天软件
关于套接字socket的网络通信。&聊天系统 聊天软件
|
4月前
|
网络协议 Java
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
这篇文章全面讲解了基于Socket的TCP网络编程,包括Socket基本概念、TCP编程步骤、客户端和服务端的通信过程,并通过具体代码示例展示了客户端与服务端之间的数据通信。同时,还提供了多个案例分析,如客户端发送信息给服务端、客户端发送文件给服务端以及服务端保存文件并返回确认信息给客户端的场景。
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
|
7月前
|
网络协议 算法 网络性能优化
网络编程:TCP/IP与套接字
网络编程:TCP/IP与套接字
|
7月前
|
网络协议 网络架构 Python
Python 网络编程基础:套接字(Sockets)入门与实践
【5月更文挑战第18天】Python网络编程中的套接字是程序间通信的基础,分为TCP和UDP。TCP套接字涉及创建服务器套接字、绑定地址和端口、监听、接受连接及数据交换。UDP套接字则无连接状态。示例展示了TCP服务器和客户端如何使用套接字通信。注意选择唯一地址和端口,处理异常以确保健壮性。学习套接字可为构建网络应用打下基础。
68 7
|
6月前
|
网络协议 Java API
网络编程套接字(4)——Java套接字(TCP协议)
网络编程套接字(4)——Java套接字(TCP协议)
51 0
|
6月前
|
Java 程序员 Linux
网络编程套接字(3)——Java数据报套接字(UDP协议)
网络编程套接字(3)——Java数据报套接字(UDP协议)
51 0
|
6月前
|
网络协议 API
网络编程套接字(2)——Socket套接字
网络编程套接字(2)——Socket套接字
33 0
|
6月前
网络编程套接字(1)—网络编程基础
网络编程套接字(1)—网络编程基础
29 0