网络编程 socket

网络编程 套接字(socket)

真正的网络通信过程,本质其实是进程间通信。 将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程。

因此,仅仅有ip地址是无法完成通信的,有了ip地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。

IP地址+端口号称为套接字(SRC_IP+SRC_PORT -> 套接字),因此称为套接字编程。

1. 认识端口号

端口号,标识特定主机上的网络进程的唯一性。

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

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

不是所有的进程都需要端口号。

2. TCP协议

TCP(Transmission Control Protocol 传输控制协议)

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

3. UDP协议

UDP(User Datagram Protocol 用户数据报协议)

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

4. 网络字节序列

image-20230630211618012

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>

unit32_t htonl(unit32_t hostlong);  //将32位的长整数从主机字节序转换为网络字节序
unit16_t htons(unit16_t hostshort); //将16位的短整数从主机字节序转换为网络字节序
unit32_t ntohl(unit32_t netlong);   //将32位的长整数从网络字节序转换为主机字节序
unit16_t ntohs(unit16_t netshort);  //将16位的短整数从网络字节序转换为主机字节序
  • h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

5. 常见的套接字

  1. 域间socket (本主机内进程间通信,也被称为一种基于套接字接口的管道通信策略)
  2. 原始socket(不常见,通常用来编写工具,允许绕传输层直接使用网络层,或绕过其他直接使用底层)
  3. 网络socket
    理论上,上面三种套接字,是三种应用场景,对应的应该是三套接口。
    但实际上为了设计简单化,将所有的接口进行统一。

6. socket编程接口

网络通信的套接字的标准是基于process的。

6.1 socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);  //backlog:全链接队列长度
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);//成功返回一个整数,表示接受的socket
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 网络发送接口(tcp)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
socket函数

**创建套接字的一个系统调用接口。**属于计算机网络提供的一个系统调用接口,是对传输层做了相关的一层文件系统级别的封装的接口。

成功返回创建的套接字文件描述符,失败返回-1。

image-20230701212140368

  • domain:指定协议族,常用的有AF_INET(IPv4)和AF_INET6(IPv6)。(网络通信or本地通信?)
  • type:套接字类型,常用的有SOCK_STREAM(流套接字,提供可靠的、面向连接的通信)和SOCK_DGRAM(数据报套接字,提供不可靠的、无连接的通信)。
  • protocol:指定协议,一般为0,表示根据domain和type自动选择合适的协议。
recvfrom函数

recvfrom函数是用于接收数据的系统调用函数,它从udp套接字接收数据,并将其存储到指定的缓冲区中。recvfrom函数的返回值是一个整数,表示接收到的数据的字节数。

image-20230701212057033

  • 如果返回值大于0,表示成功接收到数据,并返回接收到的字节数。
  • 如果返回值为0,表示对方已经关闭了连接。
  • 如果返回值为-1,表示接收数据出现错误,可以通过检查errno来获取具体的错误原因。
sendto函数

发送数据到指定的目标地址

image-20230702101223126

注意这里的addrlen不是指针,所以传的时候不用传地址。

read函数 从tcp socket中读取接收数据

image-20230708114748562

同理,在tcp socket中,使用write进行写。

6.2 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等,然而,各种网络协议的地址格式并不相同。

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结构体指针做为
参数。

基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息:地址类型,端口号,IP地址。

image-20230715154614970

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

image-20230715154653771

6.3 地址转换函数

  • 字符串转in_addr
    image-20230715155050436

  • in_addr 转字符串

    image-20230715155131968

其中inet_ptoninet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr

6.4 udp socket实例

  1. 下面是一个简单的echo server例子。echo server:client给server发送消息,server原封不动返回

udp_server.hpp

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer{
public:
    UdpServer(uint16_t port, std::string ip="0.0.0.0") :_port(port), _ip(ip), _sock(-1)
    {}
    bool initServer(){
        //从这里开始,就是新的系统调用,来完成网络功能
        //1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sock < 0){
            logMessage(FATAL, "%d : %s", errno, strerror(errno));
            exit(2);
        }
        //2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        // "192.168.1.3" -> 点分十进制字符串风格的IP地址
        //每一个区域取值范围是[0-255]:1字节 2^8 -> 4个区域
        //点分十进制字符串风格的IP地址 <-> 4字节
        struct sockaddr_in local;

        bzero(&local, sizeof(local)); //当前字段清零
        local.sin_family = AF_INET;//ipv4 协议家族/域
        //服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port);  //转参
        //1. 同上,点分十进制先要将字符串风格的IP地址-> 4字节IP
        //2. 4字节主机序列 -> 网络序列
        //有一套接口,可以一次帮我们做完这两件事情
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        //更改为从任意IP中获取信息
        //local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        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));
        //done
        return true;
    }
    void Start(){
        //作为一款网络服务器,永远不退出的!所以是一个死循环
        //服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
        //echo server:client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        for( ; ;){
            //注意:
             //peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            //输入:peer缓冲区大小
            //输出:实际读到的peer
            socklen_t len = sizeof(peer);  //输入输出型参数

            //start 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0){
                buffer[s] = 0; //目前数据当做字符串
                uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

            }
            //分析和处理数据, TODO
            //end. 写回数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);

        }
    }
    ~UdpServer()
    {
        if(_sock >= 0) close(_sock);
    }
private:
    //一个服务器,一般必须需要ip地址和port
    uint16_t _port;
    std::string _ip;
    int _sock;
};
#endif

udp_server.cc

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

static void usage(std::string proc){
    std::cout << "\nUsage: " << proc << "ip port\n" << std::endl;
}
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;
}

image-20230702105032422

完善client端代码。

client.cc

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

static void usage(std::string proc){
    std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}
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);
    }
    //client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候选择)
    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);
        //当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;
}

测试结果:

image-20230702142853286

以上就实现了本地间的通信。

通过netstat查看网络连接等信息。

image-20230702145422244

本地回环地址 127.0.0.1

127.0.0.1 是一个特殊的 IP 地址,被称为本地回环地址(loopback address)。它通常用于本机测试和网络通信的目的。

当你在计算机上使用 127.0.0.1 作为目标地址时,数据将被发送到本机的网络堆栈,而不会通过网络接口发送出去。这使得你可以在本机上模拟网络通信,而无需实际连接到外部网络。

本地回环地址 127.0.0.1 在 IPv4 中被保留,而在 IPv6 中,本地回环地址是 ::1

在网络编程中,可以使用 127.0.0.1 来测试和调试网络应用程序,或者在本地搭建服务器和客户端进行通信。

  1. 通过popen模拟shell,client发出命令,server处理并返回执行结果
void Start(){
    //作为一款网络服务器,永远不退出的!所以是一个死循环
    //服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
    //echo server:client给我们发送消息,我们原封不动返回
    char buffer[SIZE];
    for( ; ;){
        //注意:
        //peer,纯输出型参数
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        //输入:peer缓冲区大小
        //输出:实际读到的peer
        socklen_t len = sizeof(peer);  //输入输出型参数

        char result[256];
        std::string cmd_echo;
        //start 读取数据
        ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(s > 0){
            buffer[s] = 0; //目前数据当做字符串
            if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr){
                std::string err_message = "坏人...."; 
                std::cout << err_message << buffer << std::endl;
                sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr*)&peer, len);
                continue;
            }
            FILE *fp = popen(buffer, "r");
            if(nullptr == fp){
                logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                continue;
            }

            while (fgets(result, sizeof(result), fp) != nullptr)
            {
                cmd_echo += result;
            }
            fclose(fp);

        }
        //分析和处理数据, TODO
        //end. 写回数据
        // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);

    }

}
}

  1. 使用多线程实现客户端之间通讯

client.cc

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

uint16_t serverport = 0;
std::string serverip;

// int sock = -1;
static void usage(std::string proc){
    std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}

static void *udpSend(void *args){
    
    int sock = *(int*)((ThreadData*)args)->args_;    //线程套接字
    std::string name = ((ThreadData*)args)->name_;   //线程名

    std::string message;
    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());
    //不断进行发送
    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        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));
    }
    return nullptr;
}
static void *udpRecv(void *args){
    int sock = *(int*)((ThreadData*)args)->args_;    //线程套接字
    std::string name = ((ThreadData*)args)->name_;   //线程名

    char buffer[1024];
    while(true){
        //初始化为0
        memset(buffer, 0, sizeof(buffer));
        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 << buffer << std::endl;
        }
    }

}

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);
    }
    //初始化port和ip
    serverport = atoi(argv[2]);
    serverip = argv[1];
                                    /*线程编号,回调方式,传入套接字*/
    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void*)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void*)&sock));
    // sender->name();
    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);
    return 0;
}

udp_server.hpp

void Start(){
    //作为一款网络服务器,永远不退出的!所以是一个死循环
    //服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
    //echo server:client给我们发送消息,我们原封不动返回
    char buffer[SIZE];
    for( ; ;){
        //注意:
        //peer,纯输出型参数
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        //输入:peer缓冲区大小
        //输出:实际读到的peer
        socklen_t len = sizeof(peer);  //输入输出型参数

        char result[256];
        char key[64];
        std::string cmd_echo;
        //start 读取数据
        ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(s > 0){
            buffer[s] = 0; //目前数据当做字符串
            uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换
            std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址

            //简单的用户托管
            snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
            logMessage(NORMAL, "key: %s", key);
            auto it = _users.find(key);
            if(it == _users.end()){
                //not exits
                logMessage(NORMAL, "add new user : %s", key);
                _users.insert({key, peer});
            }

        }
        for(auto &iter : _users){
            /*key,即前面的用ip和port来标识一个用户的身份*/
            std::string sendMessage = key;
            sendMessage += "#";
            sendMessage += buffer;  //127.0.0.1-1234# 你好
            logMessage(NORMAL, "push message to %s", iter.first.c_str());
            sendto(_sock, sendMessage.c_str(), sendMessage.size(),0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
        }

    }
}

image-20230706224716087

注意:

  • 云服务器无法直接绑定公网IP非127.0.0.1非0.0.0.0这样的IP,即云服务器无法绑定公网IP。

  • 对于服务器来讲,不推荐绑定一个确定的IP,推荐使用绑定任意IP的方案。 即让服务器在工作过程中,可以从指定端口中的任意IP中获取数据。image-20230702150414085

    image-20230702151552623

  • 无论是多线程读还是写,用的sock都是一个,sock代表一个文件,可以同时进行收发的原因

    • UDP是全双工的,可以同时进行收发而不受干扰

6.5 tcp实例

可以通过对回调函数service的不同实现,来实现server端对client端的通信返回内容。下面是三个对回调函数不同实现的例子。

  • static void service( ); //简单的echoserver

  • static void change( ); //对于client发送到server的所有小写字母,转换为大写字母,再返回

  • static void dictOnline( ); //实现一个小词典

同样,当获取连接成功时,也列举了四种方式进行通信服务。

tcp_server.hpp

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

// // 接收到客户端数据后进行的处理
static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    char buffer[1024];
    while (true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将对应的发过来的数据当作字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
        }
        else if (s == 0) // 读取到返回值为0,代表对端关闭连接
        {
            logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else // 异常
        {
            logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
            break;
        }
        // 读取成功,继续向socket写入
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    // 大小写转换

    char buffer[1024];

    // read && write 可以直接被使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将对应的发过来的数据当作字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
        std::string message;
        char *start = buffer;
        while (*start)
        {
            char c;
            if (islower(*start))
                c = toupper(*start);
            else
                c = *start;
            message.push_back(c);
            start++;
        }
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 读取到返回值为0,代表对端关闭连接
    {
        logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
    }
    else // 异常
    {
        logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
    }
    // 读取成功,继续向socket写入
    write(sock, buffer, strlen(buffer));
    close(sock);
}

static void dictOnline(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
        {"apple", "苹果"},
        {"bite", "比特"},
        {"banana", "香蕉"},
        {"hard", "好难啊"}
    };

        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将对应的发过来的数据当作字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
            std::string message;
            auto iter = dict.find(buffer);
            if(iter == dict.end()) message = "我不知道...";
            else message = iter->second;
            write(sock, message.c_str(), message.size());
        
        }
        else if (s == 0) // 读取到返回值为0,代表对端关闭连接
        {
            logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
        }
        else // 异常
        {
            logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
        }
        // 读取成功,继续向socket写入
    close(sock);
}
// class ThreadData{
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;

// };

class TcpServer
{
private:
    const static int gbacklog = 20; // 一般不能太大也不能太小
    // static void *threadRoutine(void *args){
    //     //线程分离
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     service(td->_sock, td->_ip, td->_port);
    //     delete td;
    // }

public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {
    }
    void initServer()
    {
        // 1. 创建套接字 -- 进程和文件(以进程方式打开了一个文件)
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 打印出创建的套接字,此处为流式
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock);

        // 2. bind -- 文件 + 网络
        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);
        // 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()
    {
        // signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出的时候,自动退出僵尸状态
        // SIGCHLD:子进程终止或停止时,父进程收到的信号
        _threadpool_ptr->run(); // push任务
        while (true)
        {
            // sleep(1);
            // 4. 获取链接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            //(servicesock)通过每一次获得的新连接进行IO vs (_sock)j监听socket:获取新连接
            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);
            // 开始进行通信服务

            // version4 --线程池版本
            Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);

            // version3 --多线程版本
            // ThreadData *td = new ThreadData();
            // td->_sock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // pthread_create(&tid, nullptr, threadRoutine, td);

            // version2.1 --多进程版,不使用signal
            // pid_t id = fork();
            // if(id == 0){
            //     //child
            //     close(_listensock);
            //     if(fork() > 0) exit(0); //子进程本身立即退出
            //     //孙子进程被os接管
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // //parent
            // waitpid(id, nullptr, 0); //因为上面的子进程直接退出,所以不会造成阻塞
            // close(servicesock);

            // version 1 -- 单进程循环版 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的
            // service(servicesock, c lient_ip, client_port);

            // version 2 -- 多进程版 -- 创建子进程
            // 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd
            //  pid_t id = fork();
            //  assert(id != -1);
            //  if(id == 0){
            //      //子进程
            //      //子进程会继承父进程打开的文件与文件fd,因此此时需要关闭不需要的套接字
            //      close(_listensock);
            //      service(servicesock, client_ip, client_port);
            //      exit(0);
            //  }

            // 父进程
            //  close(servicesock); //不关闭会造成文件描述符泄漏
        }
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    std::string _ip;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

tcp_client.cc

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

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

// ./tcp_client targetIp targetPort
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]);
    bool alive = false;
    int sock = 0;
    std::string line;
    while (true)
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }

            // 此处客户端依然不需要bind,不需要显示的bind,但是一定是需要port
            // 需要让OS自动进行port选择
            // 要有连接别人的能力
            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 << "connect error" << std::endl;
                exit(3);
            }
            std::cout << "connect success" << std::endl;
            alive = true;
        }
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        if (line == "quit")
            break;

        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if (s > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        else
        {
            alive = false;
            close(sock);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值