网络编程套接字

认识端口号

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.
  • 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;

认识UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

认识TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
    在这里插入图片描述
    为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
    在这里插入图片描述
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

socket编程接口

在这里插入图片描述

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
  • 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

简单的UDP网络程序

服务端

udp_server.hpp

#pragma once

#include "log.hpp"
#include<iostream>
#include<cstdio>
#include<string>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#define SIZE 1024

class UdpServer
{
public:
    // 绑定任意ip后, ip参数可以不写
    UdpServer(uint16_t port, std::string ip = "0.0.0.0"):_port(port),_ip(ip),_sock(-1)
    {}

    // 这里是系统调用,来完成网络功能
    bool initServer()
    {
        // 1. 创建套接字
        // int socket(int domain, int type, int protocol);
        // 参数1: 套接字类型- 网络通信或本地通信
        // 参数2: 套接字的类别 - 面向数据报或面向数据流
        //【与参数1区别:在确定以网络通信或本地通信后,是再以面向数据报还是面向数据流的方式通信呢】
        // 参数3: 前两个参数确定后,就已经确定通信的协议了, 所以此参数忽略
        _sock = socket(AF_INET, SOCK_DGRAM, 0);//网络通信以数据报的形式
        if(_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        // 参数1: 将ip和port与套接字关联
        // 参数2: struct sockaddr *(通用格式) 网络通信强转(struct sockaddr_in*) 本地通信强转(struct sockaddr_un*)
        struct sockaddr_in local; // 里面需要填写三个参数 - 地址类型, 端口号, IP地址
        bzero(&local, 0); // 初始化
        local.sin_family = AF_INET; // (socket中是网络通信)结构体对象将会与网络服务器绑定
        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!网络是大端字节序
        local.sin_port = htons(_port); // 主机序列转网络序列
        //传入的ip是点分十进制字符串风格的IP地址 "192.168.110.132" 
        // sin_addr是一个结构体里面的s.addr是 uint32_t类型的 且ip为4字节的(所以上面ip需要转为4字节整形的网络序列)
        // local.sin_addr.s_addr =  inet_addr(_ip.c_str()); // inet_addr函数可以直接完成上面两步


        //【服务器建议绑定任意ip】让服务器在工作过程中,可以从任意IP中获取数据
        // 因为: 一台主机会存在多个ip,当绑定具体的ip后,服务器就只能收到发给这个具体ip的消息,如果绑定任意ip,那么
        // 就是告诉操作系统,只要是发给这台主机的指定端口的任意消息都可以接受
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // INADDR_ANY:宏就是0


        
        if(bind(_sock,(struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);  
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }
    void start()
    {
        // 作为一款网络服务器,永远不退出的!
        // 服务器启动-> 进程启动 -> 是一个常驻进程 -> 永远在内存中存在,除非挂了!
         char buffer[SIZE];
         for(;;)
         {
            // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
            // 参数1:自己的套接字, 参数2:读取的信息放在自定义的缓冲区里面,参数3:缓冲区的长度,参数4:阻塞读取或非阻塞读取
            // 参数5:接受到别人发给我的信息,我有可能还要回复信息,那么就需要知道对方的ip和port,所以src_addr里放的是别人的ip/port
            // 参数5:是输出型参数, 需要自己定义一个这样的结构体, 让对方把信息填进去,这样自己就可以对方的ip和port
            
            struct sockaddr_in peer; // peer,纯输出型参数
            bzero(&peer, sizeof(peer));
            // 【输入时len代表peer缓冲区大小,给别人大小】
            // 【输出时len代表peer被对方填充后的大小】
            socklen_t len = sizeof(peer); // 输入输出性参数
            // start. 读取数据 flag: 阻塞方式
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); 
            
            if(s > 0)
            {
                // 【发生成功进来】
                buffer[s] = 0; // 读入结尾没有\0,需要自己加将数据当作一个字符串
                // 将对方的ip和port填入peer
                uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来的!-- 需要网络转主机
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP -转- 本主机的字符串风格的IP,方便显示
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
            }
            
            /*recvfrom收到了数据这里可以做处理数据*/

            // 从peer知道对方的ip和port,可以给方发消息 sendto
            // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
            // 参数1: 自己的套接字,参数2: 存放发的消息,参数3:消息大小,参数3:是否阻塞,参数5: 发给谁,前面recvfrom里的peer就有了
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len); // 这里是直接将收到的原封不动的发回去
         }

    }
    ~UdpServer()
    {
        if(_sock >= 0)
            close(_sock);
    }
private:
    // 一个服务器必须要有ip地址和port(16位的整数)
    std::string _ip;
    uint16_t _port;
    int _sock;
};

udp_server.cc

#include "udp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    // std::cout<<"\nUsage: " << proc << " ip port\n" <<std::endl; // server绑定任意ip只需要port就可以了
    std::cout<<"\nUsage: " << proc << " port\n" <<std::endl;
}

// // ./udp_server ip port
// int main(int argc, char* argv[])
// {
//     if(argc != 3)
//     {
//         usage(argv[0]); // 出错将使用手册打印出来
//         exit(1);
//     }

//     std::string ip = argv[1];
//     uint16_t port = atoi(argv[2]);
//     std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));
    
//     svr->initServer();
//     svr->start();
//     return 0;
// }

//【绑定任意ip,服务端server只需要port】
// ./udp_server port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]); // 出错将使用手册打印出来
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    
    svr->initServer();
    svr->start();
    return 0;
}

客户端

udp_client.cc

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}

// 解释 ./udp_client 127.0.0.1 8080 中  127.0.0.1
// 127.0.0.1用于本地回环: client和server发送数据只在本地协议栈中进行数据流动,不会把数据发送到网络中, 用于本地网络服务器的测试
// 注意: 云服务器无法绑定具体ip,如./udp_client 127.128.112.123 8080 或 0.0.0.0等非127.0.0.1的ip
//       服务器也不建议绑定具体的ip,建议绑定任意ip 具体看udp_server写法
// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // server结构用于存放对方的ip和port
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if(message == "quit") break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    close(sock);
    return 0;
}

地址转换函数

基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
在这里插入图片描述
in_addr转字符串的函数:
在这里插入图片描述
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。

关于inet_ntoa:
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

  • man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

  • 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
    在这里插入图片描述
    因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

  • 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?

  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;

  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;

  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

多线程调用inet_ntoa代码示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }
    return NULL;
}
void *Func2(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}
int main()
{
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    pthread_create(&tid1, NULL, Func1, &addr1);
    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

简单的TCP网络程序

服务端

写法一

#pragma once
#include<iostream>
#include<string>

#include<cstring>
#include<cerrno>
#include<unistd.h>

#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include "log.hpp"

static void service(int sock, const std::string& clientip, const uint16_t& clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer)-1); // 相当于udp recvfrom
        if(s > 0)
        {
            buffer[s] = 0; //将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if(s == 0) //对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        { 
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer)); // 先当于 udp sendto
    }
    close(sock); // 这里的sock是serversock,一个客户端对应一个,当不再与这个客户端通信,需要关闭
}


class TcpServer
{
private:
    // listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多
    // 的连接请求就忽略, 这里设置不会太大(一般是5), 
    const static int gbacklog = 30;
public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0"):_port(port),_listensock(-1)
    {}
    bool initServer()
    {
        // 1. 创建套接字 -- fd
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0)
        {
            logMessage(FATAL, "creat socket error, %d, %s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock); // 3
        
        // 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 = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    void start()
    {
         while (true)
        {
            // 4. 获取连接(将连接到的客户端ip和port放在src中)
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // servicesock  和 _listensock都是套接字 但是作用不一样
            // _listensock是去获取连接的(像一个招揽工)  servicesock是去提供服务的(像一个服务员)
            // 举例: _listensock不断去招揽客人, 招揽到一个客人到店里面来了,那么就需要一个servicesock服务员去服务客人,每招揽一个,就需要一个servicesock
            // 系统角度: 这里的sock就是一个文件描述符,打印_listensock是3,每次获取到一个客户端来连接服务器,就会分配一个文件描述符给servicesock
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
             // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",servicesock, client_ip.c_str(), client_port);

             // 方式一: 单进程循环 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
             service(servicesock, client_ip, client_port);
             // tcp 与 udp 中 strat 不一样
             // udp start里面只有一个循环, 不断recvfrom 然后 sendto (所有的客户端都可以给服务器发消息,且都能收到)
             // tcp start里面有两个死循环(A与B,A客户端先发消息, B再发消息,服务器只能收到A发的消息,当A客户端退出,B以前发的
             // 消息一次显示到服务器)故tcp服务器只能服务一个客户端,主要原因是service函数收发消息是一个死循环,只有此客户端退出
             // server函数结束,重新回到开始再与另一个客户端建立通信,拿到对方的ip和port
             // 【可能会想为什么不和udp一样,收发消息不使用死循环】
             // 因为tcp是需要和客户端建立连接的,而udp不需要,udp通信只要拿到服务器ip+port就可以直接给udp服务器发消息 
             // udp是不需要管客户端的存活
             // tcp会给每个客户端分配一个sock来服务与tcp建立连接的客户端,是需要知道客户端的死活的,所以需要一个死循环保持与已建立
             // 连接的客户端保持通信,当客户端关闭连接,tcp服务端会知道,会做出一些处理动作,并会关闭给客户端分配的sock
             // 【所以tcp需要建立多个线程,每一个线程服务一个客户端】
        }
    }       
    ~TcpServer()
    {}
private:
    uint16_t _port;
    std::string _ip;
    int _listensock;
};

写法二(对一的补充)

#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<signal.h>
#include<unistd.h>
#include<memory>
#include<cerrno>
#include<cassert>
#include<pthread.h>
#include <sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

static void service(int sock, const std::string& clientip, const uint16_t& clientport, const std::string& name) // 【方式五】调service,多了一个_name参数
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer)-1); // 相当于udp recvfrom
        if(s > 0)
        {
            buffer[s] = 0; //将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if(s == 0) //对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        { 
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer)); // 先当于 udp sendto
    }
    close(sock); // 这里的sock是serversock,一个客户端对应一个,当不再与这个客户端通信,需要关闭
}

// // ThreadData对应【方式4】
// class ThreadData
// {
// public:
//     std::string _ip;
//     uint16_t _port;
//     int _serversock;
// };

class TcpServer
{
private:
    const static int gbacklog = 20; // 后面再说

    // threadRoutine函数对应【方式4】 【方式5】不需要
    // static void *threadRoutine(void *args)
    // {
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     service(td->_serversock, td->_ip, td->_port);
    //     delete td;

    //     return nullptr;
    // }

public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
    : _port(port),
    _ip(ip), 
    _listensock(-1),
    _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}

    bool initServer()
    {
        // 1. 创建套接字 -- fd
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0)
        {
            logMessage(FATAL, "creat socket error, %d, %s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock); // 3
        
        // 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 = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
        return true;
    }
    void start()
    {
        // signal(SIGCHLD, SIG_IGN);  // 【方式二】

        _threadpool_ptr->run();       // 【方式五】

        while (true)
        {
            // 4. 获取连接(将连接到的客户端ip和port放在src中)
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // servicesock  和 _listensock都是套接字 但是作用不一样
            // _listensock是去获取连接的(像一个招揽工)  servicesock是去提供服务的(像一个服务员)
            // 举例: _listensock不断去招揽客人, 招揽到一个客人到店里面来了,那么就需要一个servicesock服务员去服务客人,每招揽一个,就需要一个servicesock
            // 系统角度: 这里的sock就是一个文件描述符,打印_listensock是3,每次获取到一个客户端来连接服务器,就会分配一个文件描述符给servicesock
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
             // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",servicesock, client_ip.c_str(), client_port);


            //  // 【方式一】: 单进程循环 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            //  service(servicesock, client_ip, client_port);
            

            // // 【方式二】- 多进程版 --- 创建子进程
            // // 让子进程给新的连接提供服务,子进程可以打开父进程曾经打开的文件fd
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程, 子进程会继承父进程打开的文件与文件fd(servicesock与_listensock)
            //     // 子进程是来进行提供服务的,不需要监听socket,所以不需要_listensock,要关闭
            //     close(_listensock);
            //     service(servicesock, client_ip, client_port);
            //     exit(0); // 僵尸状态
            // }
            // close(servicesock); // 如果父进程关闭servicesock,不会影响子进程,且父进程用不到servicesock,所以要关闭
            // // waitpid(id, nullptr, 0); // 不能阻塞,要不然有和单线程版的server一样了
            // // 但是不阻塞,需要一个vector来存放所有子进程的pid,然后循环join,回收资源
            // // 使用vector太麻烦了,【好办法:使用信号】-- 放在start函数开始
            // // signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态


            // // 【方式三】多进程版 --- 创建子进程 -- 不适用信号
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0/*子进程本身*/) exit(0); //子进程本身立即退出
            //     // 孙子进程, 孤儿进程,OS领养, OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); //0:虽然是阻塞等待,但是不会阻塞,因为子进程已近退出了
            // close(servicesock);

            // // 【方案四】多线程
            // ThreadData *td = new ThreadData();
            // td->_serversock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t pid;
            // pthread_create(&pid, nullptr, threadRoutine, td);
            // // 在多线程这里不用进程关闭_listensock,但要关闭servicesock, servicesock在service里面关
            // // close(servicesock);


            // 【方案五】线程池版本的多线程
            Task t(servicesock, client_ip, client_port, service);
            _threadpool_ptr->pushTask(t);
        }

    }       
    ~TcpServer()
    {}
    
private:
    uint16_t _port;
    std::string _ip;
    int _listensock;

    // 【方案五】线程池版本的多线程 -- 需要添加一个成员
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

客户端

#include<string>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include "ThreadPool/log.hpp"

static void Usage(const std::string proc)
{
    std::cout<<"\nUsage: " << proc << " tcpserver ip port" << std::endl;
}

// ./tcp_client ip port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std:: string serverip  = argv[1];
    uint16_t serverport  = atoi(argv[2]);

    // 1. 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        std::cerr<<"socket error"<<std::endl;
        exit(2);
    }
    // client 不需要显示的bind,但是一定是需要port
    // 需要让os自动进行port选择

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport );

    // 2. 连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3); 
    }
    std::cout << "connect success" << std::endl;

    // 3. 收发消息
    while(true)
    {
        std::string line;
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        if(line == "quit")
            break;
        
        // connect后可以直接使用send与recv
        ssize_t s = send(sock, line.c_str(), line.size(), 0); // 0阻塞式通信

        if(s > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            buffer[s] = 0;
            std::cout << "server 回显# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            break;
        }
        else 
        {
            break;
        }
    }



    return 0;
}

链接

UDP/TCP相关完整代码

TCP协议通讯流程

在这里插入图片描述
在这里插入图片描述

TCP 和 UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

将套接字相关的接口封装

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>

class Sock
{
private:
    // listen的第二个参数,意义:底层全连接队列的长度 = listen的第二个参数+1
    const static int gbacklog = 10;
public:
    Sock() {}
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            exit(2);
        }
        // 先断开链接一方会进入TIME_WITE状态,port依旧被占着,bind再次绑定会失败,setsockopt可以让bind成功
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return listensock;
    }
    static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(3);
        }
    }
    static void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            exit(4);
        }

    }
    // 一般经验
    // const std::string &: 输入型参数
    // std::string *: 输出型参数
    // std::string &: 输入输出型参数
    static int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            return -1;
        }
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }
    static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }
    ~Sock() {}
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值