网络:udptcp套接字

目录

协议

网络传输基本流程

网络编程套接字

udp套接字编程

 udp相关代码实现

sock函数

bind函数

recvfrom函数

sendto函数

udp执行指令代码

popen函数

udp多线程版收发消息

tcp套接字编程

tcp套接字代码

listen函数

accept函数

read/write函数

connect函数

recv/send函数


协议

协议本质就是一种约定,网络上一定要硬件上有标志,软件上有协议

关于协议分层

1.操作系统要进行协议管理:先描述,在组织
2.协议本质就是软件,软件是可以”分层‘
3.协议在设计的时候,就是被层状的划分的
4.要划分为层状结构是因为: a. 场景复杂 b.功能解耦,便于人们进行各种维护

所以网络协议也是层状的

OSI七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层这7层

但是, OSI七层模型既复杂又不实用; 所以我们按照TCP/IP四(五)层模型来讲解,即(物理层)、数据链路层、网络层、传输层、应用层

TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求,物理层我们不关心

传输层和网络层是在操作系统中实现的

对于一台主机, 它的操作系统内核实现了从传输层到网络层
对于一台路由器, 它实现了从网络层到物理层
对于一台交换机, 它实现了从数据链路层到物理层
对于集线器, 它只实现了物理层


网络传输基本流程

下面几层对应的代表性的协议

网络通信的过程中:

在逻辑上:在应用层相互直接传输
在物理上: 必须得自顶向下(从应用层->链路层)将数据报文交付到最底层,然后在以太网上传输,被你的主机收到后,再自底向上(从链路层->应用层)交付到应用层

每层都有自己的协议定制方案,
每层协议都要有自己的协议报头
从上到下交付数据的时候,要添加报头
从下到上递交数据的时候,要去掉报头

协议报头就是收到的报文当中,多出来的数据内容,可以理解为同层协议提前约定好的共识字段

把自顶向下交付的过程,称之为封装的过程,封装的本质:添加报头

自底向上交付的时候,需要将报头有效载荷做分离,分析报头的字段,去决定将它的有效载荷向上应该交付给哪一个协议

把自底向上交付的过程,称之为解包的过程,解包的本质:去掉报头,展开分析


在局域网中两台主机是可以直接通信的

例如有5台主机都在一个局域网里,主机1和主机2通信,表面上是这两个主机单独通信,实际其他主机也都能看到他们通信的内容,只是每个主机对比自己的MAC地址,看是不是再和自己通信,如果不是,将当前报文丢弃即可

通过MAC地址来标识局域网中主机的唯一性

IP地址和MAC地址的区别:

IP就类似于旅游时,想从陕西去新疆去,所以此时的源IP就是陕西,目的IP就是新疆

在去的路途中,加入当前在甘肃省,当地人说下一站应该去青海省,所以此时的源MAC地址就是甘肃,下一站MAC地址就是青海

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


在使用TCP/IP协议的网络中,IP及其向上的协议,看到的报文都是一样的。

应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部,称为封装

数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议 字段" 将数据交给对应的上层协议处理.


网络编程套接字

网络4件套,只要将下面四种头文件都包含,网络中所需要的就都能使用了

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <arpa/inet.h>


IP协议有两个版本, IPv4和IPv6,没有特殊说明的, 默认都是指IPv4,IPv4来说, IP地址是一个4字节, 32位的整数

IP地址,就例如:192.168.0.1,用点分割的每一个数字表示一个字节, 范围是 0 - 255;所以理论上,表示一个IP地址,其实4字节就够了,而192.168.0.1这个字符串却需要11个字节,所以点分十进制字符串风格的IP地址,需要和4字节的IP地址互相转换

IP地址(公网IP),它标定了主机的唯一性

端口号一般是16位的整数

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

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

所以IP+端口号就可以得到全网唯一的一个进程了

我们把IP地址+端口号,称之为套接字


 UDP协议

UDP协议叫做用户数据报协议,它的特点:无连接、不可靠传输、面向数据报

无连接:在使用UDP通信时,不用写代码时建立连接,直接发送数据即可

不可靠传输:UDP可能出现网络丢包的问题,或是数据包乱序重复等问题


TCP协议

TCP协议叫做用户数据报协议,它的特点:有连接、可靠传输、面向字节流

可靠传输:指TCP不会出现上述UDP的网络丢包等问题


那么UDP是不可靠传输,而TCP是可靠传输,为什么我们还需要学UDP呢,直接学TCP就好了呀

其实很简单,TCP保证可靠性,就一定决定了它在数据通信的过程中,要为了保证可靠性,而设计更多的策略,各种制度机制,都要靠协议自己完成,往往更复杂,成本更高

而UDP只要将数据发出去,就不用管可不可靠的问题了,UDP更为简单,成本更低,TCP则更为安全,所以UDP/TCP是各有所长的

因此在应用场景中,除非场景非常适合UDP,那么我们都选择TCP协议

像直播、视频一类的使用UDP更好,例如在看直播时,突然人物卡顿一下,掉帧一下,都是因为使用的UDP,导致的丢包问题,但是并不会影响我们观看直播


网络字节序

假设两个主机通信,一个主机是大端存储,一个主机是小段存储,那么在通信时,肯定会发生错误,因为两个主机存储的方式都不一样,肯定无法正确的通信

所以计算机网络规定,所有网络数据,都必须是大端存储的

操作系统提供了4个接口,头文件是arpa/inet.h

uint32_t htonl(uint32_t hostlong) ;
uint16_t htons(uint16_t hostshort) ;
uint32_t ntohl(uint32_t netlong) ;
uint16_t ntohs(uint16_t netshort) ;

其中h表示host,表示本地;n表示network,表示网络;l表示long,4字节;s表示short,2字节

所以hton就表示主机转网络,即无论主机是什么序列都会转成大端

ntoh就表示网络转主机


常见套接字:

域间socket、原始socket、网络socket

其中网络socket是使用的最多

理论上,这三种套接字是三种应用场景,对应的应该是三套接口,而Linux为了使用更方便,不想设计过多的接口,而是使用同一套接口,如下所示:

创建 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);

接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

通过观察可以发现,每一个接口都有一个共同的结构sockaddr

其中网络socket是sockaddr_in结构,它的前两个字节是AF_INET

域间socket是sockaddr_un结构,它的前两个字节是AF_UNIX

那么这个公共的结构sockaddr,前两个字节就可以用来辨别是网络socket还是域间socket,sockaddr就如同我们C语言中的void*的概念


udp套接字编程

 udp相关代码实现

下面初步实现服务端与客户端链接,客户端发消息,服务端可以回复消息,如下所示:

 下面是几个需要使用的函数接口:

sock函数

sock函数是计算机网络给我们提供的系统调用接口,对传输层做了文件系统级别封装的接口

socket函数需要包含头文件sys/types.h和sys/socket.h

函数返回值返回一个文件描述符,返回-1就表示失败,errno就表明出错的原因

函数参数

第一个参数domain:一般表示套接字的域,有AF_INET(网络通信)、AF_UNIX(本地通信)等等

第二个参数type:type表示类型,代表你想创建这个套接字的通信种类是什么,一般填的是SOCK_DGRAM,表示用户数据报套接字

第三个参数protocol:protocol表示协议,一般前两个参数确定了,协议也就确定了,所以一般是0

使用示例如下:


bind函数

bind函数:将用户设置的ip和port在内核中和我们当前的进程强关联

bind函数需要包含头文件sys/types.h和sys/socket.h

函数返回值:成功返回0,否则返回-1,errno就表明出错的原因

函数参数

第一个参数sockfd:是刚刚创建的套接字,需要将ip和port与套接字关联起来

第二个参数addr:上面提到了填充ip和port信息的结构体

第三个参数addrlen:是addr的大小,用sizeof就能计算出来

使用示例如下:

因为bind接口的第二个参数类型是struct sockaddr*的,而我们创建的local的类型是struct sockaddr_in的,所以需要强转一下


recvfrom函数

recvfrom函数是读取数据的函数

函数返回值:ssize_t是有符号整数,失败会返回-1

函数参数:

第一个参数:表示刚刚创建成功的套接字

第二/三个参数:表示一段缓冲区,len表示大小

第四个参数:表示读取的方式,默认为0,表示以阻塞方式读取

第五/六个参数:表示两个输出型参数,需要我们传入特定的结构体,及特定结构体的其他信息,方便我们把客户端的IP和端口提上来,服务器就能够知道是谁发的消息,进而把消息再转发回去

除了拿到数据,我们也想知道是谁给我发的消息,即得到src_ip(源IP),src_port(源端口号),即客户端的IP和端口号,当client发送给server时,server就需要得到client_ip和client_port

使用示例如下:


sendto函数

sendto函数是返回消息的接口

第一个参数:表示创建的套接字

第二/三个参数:表示要发送的数据buf缓冲区,和它的长度len

第三个参数:flag默认设置为0

第四/五个参数:表示需要把缓冲区发给谁,长度是多少

使用示例如下:


关于127.0.0.1,这个IP地址称为本地环回,client和server发送数据只在本地协议栈中进行数据流动,不会吧我们的数据发送到网络中

通常用于本地网络服务器的测试,如果在127.0.0.1这里测试通过了,即客户端发消息,server能收到,那如果联通网络,再进行测试时,发现联不通,这时的问题大概率就是网络的问题


在云服务器中:

云服务器无法bind公网IP,也不建议bind公网IP

对于server来讲,我们也不推荐bind确定的IP

推荐使用任意IP的方案,如下

所以服务器服务端,写IP地址填充的时候,都是INADDR_ANY,当然如果未来有特殊需求,也可以在构造函数中给指定的IP,这样就bind指定的IP了


代码如下:

makefile:

.PHONY:all
all:udp_client udp_server

udp_client:udp_client.cc
	g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udp_client udp_server

udp_client.cc:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#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
    //但是一般client不会显示bind,即程序员不需要自己bind
    //client是一个客户端,是被普通人下载安装使用的
    //如果程序员自己bind了,client就bind了一个固定的port
    //万一其他客户端提前占用了这个port,该client就不会启动了
    //client一般不需要显示的bind指定port,而是让0S自动随机选择
    std::string message;        
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));//初始化为0
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    //inet_addr将点分十进制字符串转为4字节,再将4字节主机序列->网络序列
    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;
}

udp_server.hpp:

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip=""):
    _port(port),_ip(ip),_sock(-1)
    {}

    //初始化服务器
    bool initServer()
    {
        //从这里开始,就是新的系统调用,来完成网络功能
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sock < 0)//_sock < 0表示创建套接字失败
        {
            //strerror将错误码转化为错误码描述
            logMessage(FATAL,"%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        //sockaddr_in是结构体封装的整数
        struct sockaddr_in local;
        //结构体需要进行清0,使用bzero函数
        bzero(&local, sizeof(local));   
        //上面传入的参数是AF_INET,这里的sin_family也就是AF_INET
        local.sin_family = AF_INET; 
        //服务器的IP和端口未来也是要发送给对方主机的->先要将数据发送到网络
        //所以需要将_port主机转网络
        local.sin_port = htons(_port);
        //1.同上,先要将点分十进制字符串风格的IP地址-> 4字节
        //2. 4字节主机序列->网络序列
        //有一套接口inet_addr,可以一次帮我们做完上述这两件事情
        //宏INADDR_ANY就是0,让服务器在工作过程中,可以从任意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));
        //初始化结束
        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);
            //s>0表示读取数据成功
            if(s > 0)
            {
                buffer[s] = 0;//我们目前数据当做字符串
                // 1.输出发送的信息 2. 知道是谁发送的 
                //从网络中来,所以需要网络转主机,即ntoh
                uint16_t cli_port = ntohs(peer.sin_port);
                //4字节的网络序列的IP -> 本主机的点分十进制风格的IP,使用inet_ntoa函数
                std::string cli_ip = inet_ntoa(peer.sin_addr);
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
            }
            //分析和处理数据

            //end : 写回数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

    ~UdpServer()
    {
        if(_sock >= 0)
            close(_sock);//关闭套接字
    }
private:
    //一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;    
    int _sock;//通信的套接字
};


#endif


udp_server.cc:

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

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

// ./udp_server port
//命令行参数,argc是2
//argv[0]/[1]分别是./udp_server/port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    //std::string ip = argv[1];
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    //初始化服务器、启动服务器
    svr->initServer();
    svr->Start();
    return 0;
}

以及使用的以前实现的log.hpp:

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <string>
#include <ctime>

//日志等级
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

//日志等级的映射表
const char* gLevelMap[]={
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

//日志功能至少:日志等级 时间;支持用户自定义(日志内容等等)
//format是输出格式例如%s之类的
void logMessage(int level, const char* format, ...)
{
//条件编译,如果定义了DEBUG_SHOW这个宏,在打印时就正常打印
//如果没有定义这个DEBUG_SHOW宏,所以level是DEBUG的语句就不再打印了
//我们可以在makefile命令行中定义宏,加上-D选项即可
#ifndef DEBUG_SHOW
    if(level == DEBUG) return;
#endif
    //                                        vprintf/vfprintf/vsprintf/ vsnprintf
    //是把传入的参数按照可变的方式分别进行显示到 显示器 /  文件  / 字符串 /指定长度的字符串
    //stdBuffer是日志的标准部分,如日志等级 时间
    //logBuffer是日志的自定义部分,如日志内容等
    char stdBuffer[1024];
    char logBuffer[1024];

    //这里的时间采用较为简单的时间戳表示
    time_t tm = time(nullptr);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld]", gLevelMap[level], tm);

    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer,sizeof logBuffer, format, args);
    va_end(args);
    //拼接两个字符串的内容,一块打印出来
    printf("%s %s\n", stdBuffer, logBuffer);
}


udp执行指令代码

如果是上面的聊天,发送的就是文本,如果是指令例如ls -a -l,我们想把命令执行完,把执行的结果再返回,下面实现远程实现命令的功能:

popen函数

popen可以执行传入的command指令,先建立pipe管道,然后再fork(),子进程执行(exec*,即exec系列的接口)command的命令,

返回值FILE *:可以将执行结果通过FILE*指针进行读取


此功能实现,仅仅改变了udp_server.hpp,其他文件没有变化:

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip=""):
    _port(port),_ip(ip),_sock(-1)
    {}

    //初始化服务器
    bool initServer()
    {
        //从这里开始,就是新的系统调用,来完成网络功能
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sock < 0)//_sock < 0表示创建套接字失败
        {
            //strerror将错误码转化为错误码描述
            logMessage(FATAL,"%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        //sockaddr_in是结构体封装的整数
        struct sockaddr_in local;
        //结构体需要进行清0,使用bzero函数
        bzero(&local, sizeof(local));   
        //上面传入的参数是AF_INET,这里的sin_family也就是AF_INET
        local.sin_family = AF_INET; 
        //服务器的IP和端口未来也是要发送给对方主机的->先要将数据发送到网络
        //所以需要将_port主机转网络
        local.sin_port = htons(_port);
        //1.同上,先要将点分十进制字符串风格的IP地址-> 4字节
        //2. 4字节主机序列->网络序列
        //有一套接口inet_addr,可以一次帮我们做完上述这两件事情
        //宏INADDR_ANY就是0,让服务器在工作过程中,可以从任意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));
        //初始化结束
        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);
            char result[256];//将fgets读到的都放到result中
            std::string cmd_echo;
            //start : 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            //s>0表示读取数据成功
            if(s > 0)
            {
                buffer[s] = 0;//我们目前数据当做字符串
                //判断命令中不能有rm相关的指令
                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(fp == nullptr)
                {
                    logMessage(ERROR,"%d:%s", errno, strerror(errno));
                    continue;
                }

                while(fgets(result, sizeof(result), fp) != nullptr)//读取成功
                {
                    cmd_echo += result;
                }
                fclose(fp);
            }
            //分析和处理数据

            //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);
        }
    }

    ~UdpServer()
    {
        if(_sock >= 0)
            close(_sock);//关闭套接字
    }
private:
    //一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;    
    int _sock;//通信的套接字
};


#endif


udp多线程版收发消息

下面再加以改进,把所有访问当前服务器的人都记录下来,每次一个客户端发消息,另一个客户端能够收到别的客户端发送的消息

下面演示效果:有两个客户端使用mkfifo创建命名管道,分别是clientA和clientB,每一个客户端都设立两个窗口,一个窗口发数据,将收到的数据输出重定向到管道中,另一个窗口使用cat从管道读出来:

上述是4个窗口,上面两个是clientA的,下面两个是clientB的,左边的窗口发送数据,右边的窗口接收数据,并且可以根据端口号看出来是谁发的消息,例如端口号为32795的就是clientA,而端口号为44280的就是clientB

下面是服务端:

最开始运行./udp_server 8080后,直到clientA发送数据后,服务端就会打印init,key,add这三条语句,并且每当其中一个客户端发送数据,服务端都会将这条数据发送给每一个客户端,两个push语句就能够很好地体现这一点


下面是相关代码:

makefile的udp_client后面需要加-lpthread,因为要使用pthread库

并且这里引用了前面曾经实现过的thread.hpp


udp_server.hpp:

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip=""):
    _port(port),_ip(ip),_sock(-1)
    {}

    //初始化服务器
    bool initServer()
    {
        //从这里开始,就是新的系统调用,来完成网络功能
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sock < 0)//_sock < 0表示创建套接字失败
        {
            //strerror将错误码转化为错误码描述
            logMessage(FATAL,"%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        //sockaddr_in是结构体封装的整数
        struct sockaddr_in local;
        //结构体需要进行清0,使用bzero函数
        bzero(&local, sizeof(local));   
        //上面传入的参数是AF_INET,这里的sin_family也就是AF_INET
        local.sin_family = AF_INET; 
        //服务器的IP和端口未来也是要发送给对方主机的->先要将数据发送到网络
        //所以需要将_port主机转网络
        local.sin_port = htons(_port);
        //1.同上,先要将点分十进制字符串风格的IP地址-> 4字节
        //2. 4字节主机序列->网络序列
        //有一套接口inet_addr,可以一次帮我们做完上述这两件事情
        //宏INADDR_ANY就是0,让服务器在工作过程中,可以从任意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));
        //初始化结束
        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);
            char key[64];
            char result[256];//将fgets读到的都放到result中
            std::string cmd_echo;
            //start : 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            //s>0表示读取数据成功
            if(s > 0)
            {
                buffer[s] = 0;//我们目前数据当做字符串
                //提取ip和port
                uint16_t cli_port = ntohs(peer.sin_port);
                //4字节的网络序列的IP -> 本主机的点分十进制风格的IP,使用inet_ntoa函数
                std::string cli_ip = inet_ntoa(peer.sin_addr);
                snprintf(key,sizeof(key),"%s-%u",cli_ip.c_str(),cli_port);
                logMessage(NORMAL, "key:%s", key);
                auto it = _users.find(key);//查找key
                if(it == _users.end())
                {
                    logMessage(NORMAL,"add new user : %s", key);
                    //说明用户不存在
                    _users.insert({key,peer});
                }
            }
            //分析和处理数据

            //end : 写回数据
            //sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            for(auto& iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer;
                //因为直接发送消息,并不知道是谁发的消息,所以在消息前加上发送消息的 “IP+port#” 
                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));
            }
        }
    }

    ~UdpServer()
    {
        if(_sock >= 0)
            close(_sock);//关闭套接字
    }
private:
    //一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;    
    int _sock;//通信的套接字
    std::unordered_map<std::string, struct sockaddr_in> _users;
};


#endif


udp_server.cc:

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

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

// ./udp_server port
//命令行参数,argc是2
//argv[0]/[1]分别是./udp_server/port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    //std::string ip = argv[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 <cstdio>
#include <cstring>
#include <string>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "thread.hpp"

//我们发现:无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的->可以同时进行收发而不受干扰

uint16_t serverport = 0;
std::string serverip;

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

// 发送方
static void *udpSend(void *args)
{
    // 拿到sock和线程名name
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 初始化为0
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    // inet_addr将点分十进制字符串转为4字节,再将4字节主机序列->网络序列
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    // 不断的给对方发送数据
    while (true)
    {
        std::cerr << "请输入你的信息# "; //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)
    {
        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);
    }
    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(1, udpRecv, (void *)&sock));

    sender->start();
    recver->start();

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

    close(sock); // 关闭套接字

    //   client当然也需要bind
    // 但是一般client不会显示bind,即程序员不需要自己bind
    // client是一个客户端,是被普通人下载安装使用的
    // 如果程序员自己bind了,client就bind了一个固定的port
    // 万一其他客户端提前占用了这个port,该client就不会启动了
    // client一般不需要显示的bind指定port,而是让0S自动随机选择
    // std::string message;
    // struct sockaddr_in server;
    // memset(&server,0,sizeof(server));//初始化为0
    // server.sin_family = AF_INET;
    // server.sin_port = htons(atoi(argv[2]));
    // //inet_addr将点分十进制字符串转为4字节,再将4字节主机序列->网络序列
    // 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;
    //     }
    // }
    return 0;
}

thread.hpp:

#pragma once

#include <iostream>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include <queue>
#include <string>
#include <cstdio>


typedef void*(*func_t)(void*); 

//设置ThreadData是为了未来创建线程的时候,把名字也传进去
class ThreadData
{
public:
    void* args_;
    std::string name_;
};

class Thread
{
public:
    Thread(int num, func_t callback, void* args):func_(callback)
    {
        //构造函数中初始化name_
        char namebuffer[64];
        snprintf(namebuffer, sizeof namebuffer, "thread->%d", num);
        name_ = namebuffer;

        tdata_.args_ = args;
        tdata_.name_ = name_;
    }
    //创建线程
    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void*)&tdata_);
    }
    //线程等待
    void join()
    {
        pthread_join(tid_, nullptr);
    }

    std::string name()
    {
        return name_;
    }

    ~Thread()
    {}
private:
    std::string name_;
    func_t func_;
    ThreadData tdata_;
    pthread_t tid_;
};

tcp套接字编程

tcp套接字代码

tcp同样,需要socket先创建套接字:

与udp一样,第一个参数需要传AF_INET,因为是网络通信

第二个参数就与udp不同了,传入的是SOCK_STREAM,代表套接字类型是stream

第三个参数同样默认为0


listen函数

因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接,所以就需要用到listen,阿金套接字状态设置为监听状态

函数参数:

第一个参数就是创建好的套接字

第二个参数叫做全链接队列长度

返回值:

成功返回0,失败返回-1,错误码被设置

此时执行完listen函数后,在一个窗口执行./tcp_server 8080后,另一个窗口使用netstat -antp,可以观察到./tcp_server的状态是LISTEN,表示此时的服务器已经运行,且处于监听状态


accept函数

accept函数用于获取新连接

函数参数:

第一个参数:是创建好的套接字

第二/三个参数:addr是输出型参数,addrlen是输入输出型参数,在recvfrom中使用方式一样,详细见上面的recvfrom函数

返回值:

成功的话返回一个合法的整数套接字,失败返回-1,错误码被设置


read/write函数

在udp中,读取数据使用的是recvfrom函数,而tcp中不使用这个函数,读使用read,写使用write

这里的read和write在读写文件时使用过,网络这里也可以直接使用(也可以使用后面的recv/send函数)

read:

使用方式如下:

注意这里的sizeof(buffer)-1,是将buffer后面的\0减掉了

write:

使用方式如下:

注意这里的strlen算的是buffer里的字符数量,没有减去\0 


connect函数

connect是进行连接的函数,connect函数会自动绑定当前客户端的IP和端口,能够进行后续通信

函数参数:

第一个参数:套接字

第二/三个参数:代表想要连接谁,和sendto的后两个参数一样

返回值:

成功返回0,失败返回-1,错误码被设置


recv/send函数

send函数也是基于tcp向目标服务器发送消息的

它的前三个参数和write一样, 最后一个flag参数默认设为0即可

同样和recvfrom的前三个参数一样,最后一个flag参数默认设为0即可


最终可以通过改变tcp_server.hpp中的原始的server函数,从而实现不同的功能,下面实现了通信、大小写转换、英汉互译等功能,以大小写转换为例,示范:

用户端:

服务端:


代码如下:

Threadpool中的Task.hpp改变如下:

#pragma once

#include "log.hpp"
#include <iostream>
#include <functional>
#include <string>


// typedef function<void(int, const std::string&, const uint16_t&)> fun_t;
using fun_t = function<void(int, const std::string&, const uint16_t&, const std::string&)>;

class Task
{
public:
    Task()
    {}

    Task(int sock,const std::string ip,uint16_t port,fun_t func)
    :_sock(sock),_ip(ip),_port(port),_func(func)
    {}

    //仿函数
    void operator()(const std::string& name)
    {
        _func(_sock, _ip, _port, name);
    }
public:
    int _sock;      //套接字
    std::string _ip;//ip
    uint16_t _port; //端口
    fun_t _func;    
};

makefile:

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11
tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

tcp_client.cc:

#include <iostream>
#include <string>
#include <cstdio>
#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;
}

//./tcp_client serverip serverport
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)
    {
        //alive为false,表示需要创建套接字
        if (!alive)
        {
            // 创建套接字
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0) // 创建套接字失败
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            // client不需要手动bind,需要OS自动进行选择
            // client需要连接别人的能力
            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());

            // connec进行连接
            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 ret = send(sock, line.c_str(), line.size(), 0);
        //如果发送成功
        if (ret > 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);
        }
        // else if (s == 0)
        // {
        //     break;
        // }
        // else
        // {
        //     break;
        // }
    }
    return 0;
}

tcp_server.cc:

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

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

//./tcp_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<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->Start();

    return 0;
}

tcp_server.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cassert>
#include <unordered_map>
#include <memory>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "threadpool/log.hpp"
#include "threadpool/lockGuard.hpp"
#include "threadpool/Task.hpp"
#include "threadpool/threadPool.hpp"
#include "threadpool/thread.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);

//         //read成功
//         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));
//     }
//     close(sock);
// }

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);

        // read成功
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << 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));
    }
    close(sock);
}

// 改为大小写转换的方法
static void change(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    // 所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接,长连接
    //
    char buffer[1024];
    // read && write可以直接使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);

    // read成功
    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) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }
    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","苹果"},
        {"pencil","铅笔"},
        {"bus","公交车"},
        {"ruler","尺子"}
    };
    // read && write可以直接使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);

    // read成功
    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) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }
    close(sock);
}



// class ThreadDate
// {
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;
// };

class TcpServer
{
private:
    const static int gbacklog = 20; // 一般不能太大,也不能太小,后面在进行详细解释
    // 类内会有默认参数this,不符合回调函数的参数类型,所以设置为static的
    //  static void* threadRoutine(void* args)
    //  {
    //      //线程分离,设置分离状态后主线程就不需要再join线程了,线程退出时,自动释放线程资源
    //      pthread_detach(pthread_self());
    //      ThreadDate* td = static_cast<ThreadDate*>(args);
    //      service(td->_sock, td->_ip, td->_port);
    //      delete td;

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

    void initServer()
    {
        // 1. socket创建套接字 进程 + 文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (socket < 0)
        {
            logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 文件描述符012默认打开,所以再创建就是3
        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);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        // binf失败
        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()
    {
        _threadpool_ptr->run(); // 运行线程池
        // signal(SIGCHLD, SIG_IGN);//忽略SIG_CHLD信号,子进程退出时会自动释放自己的僵尸状态
        // 服务器需要周而复始的运行,所以while(true)
        while (true)
        {
            // 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // serversock和_listensock都是套接字,区别如下:
            // serversock(未来进行网络通信服务) vs _listensock(只是获取新连接)
            int serversock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (serversock < 0)
            {
                // 获取连接失败,continue继续获取连接
                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, serversock: %d | %s : %d | \n",
                       serversock, client_ip.c_str(), client_port);
            // version 4 -- 线程池版本
            // Task t(serversock, client_ip, client_port, service);
            // Task t(serversock, client_ip, client_port, change);
            Task t(serversock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);

            // //version 3 -- 多线程版本
            // ThreadDate* td = new ThreadDate();
            // td->_ip = client_ip;
            // td->_port = client_port;
            // td->_sock = serversock; 
            // pthread_t tid;
            // pthread_create(&tid, nullptr, threadRoutine, td);
            // close(serversock)

            // version 2.1 -- 多进程版本 -- 创建子进程(不使用signal)
            //  pid_t id = fork();
            //  assert(id != -1);
            //  if(id == 0)
            //  {
            //      //子进程
            //      close(_listensock);
            //      if(fork() > 0)
            //          exit(0);//子进程本身直接退出
            //      //下面的是孙子进程,孤儿进程,OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
            //      service(serversock, client_ip, client_port);
            //      exit(0);
            //  }
            //  //父进程
            //  waitpid(id, nullptr, 0);//不会阻塞,因为子进程再创建孙子进程时就exit退出了
            //  close(serversock);

            // 开始进行通信服务
            //  version 1.0 -- 单进程循环版本
            // 只能够进行一次处理一个客户端,处理完一个,才能处理下一个客户端
            // 很显然version 1 是不能直接被使用,因为是单进程的
            // 使用service函数实现
            //  service(serversock, client_ip, client_port);
            // version 2.0 -- 多进程版本 -- 创建子进程
            // 让子进程给新的连接提供服务,子进程能够打开父进程曾经打开的文件fd
            //  pid_t id = fork();
            //  assert(id != -1);
            //  if(id == 0)
            //  {
            //      //子进程会继承父进程打开的文件与文件fd
            //      //子进程是来进行提供服务的,所以不需要知道监听socket,所以close掉监听socket
            //      close(_listensock);
            //      //子进程对新的连接提供服务,父进程则继续accept新连接
            //      service(serversock, client_ip, client_port);
            //      exit(0); // 子进程退出后变为僵尸状态
            //  }
            // 父进程
            // 需要等待子进程,但是这里最好不要使用waitpid(),因为这时阻塞等待的,设置为非阻塞也比较麻烦
            // 所以这里采用信号的方式signal,子进程退出会主动向父进程发送SIG_CHLD信号,所以可以忽略SIG_CHLD信号
            // 子进程退出的时候,会自动释放自己的僵尸状态
            // close(serversock);//父进程只需要listensocket,不需要serversock,所以close掉serversock
            // 如果父进程关闭servicesock,会不会影响子进程?
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;  // 端口
    std::string _ip; // IP
    int _listensock; // 监听套接字

    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

以上即为udp、tcp套接字编程的全部内容


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/497609.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二十一章 Jquery ajax

文章目录 1. jquery下载2. jquery的使用3. jquery页面加载完毕执行4. jquery属性控制6. 遍历器 2. ajax1. 准备后台服务器2. ajax发送get请求3. ajax发送post请求 1. jquery下载 点击下载 稳定版本1.9 2. jquery的使用 存放到html文件的同级目录 3. jquery页面加载完毕执行…

verilog设计-cdc:多比特信号跨时钟域(DMUX)

一、前言 多比特一般为数据&#xff0c;其在跨时钟域传输的过程中有多种处理方式&#xff0c;比如DMUX&#xff0c;异步FIFO&#xff0c;双口RAM&#xff0c;握手处理。本文介绍通过DMUX的方式传输多比特信号。 二、DMUX同步跨时钟域数据 dmux表示数据分配器&#xff0c;该方…

HarmonyOS 应用开发之UIAbility组件生命周期

概述 当用户打开、切换和返回到对应应用时&#xff0c;应用中的UIAbility实例会在其生命周期的不同状态之间转换。UIAbility类提供了一系列回调&#xff0c;通过这些回调可以知道当前UIAbility实例的某个状态发生改变&#xff0c;会经过UIAbility实例的创建和销毁&#xff0c;…

PCB损耗来源

信号经过PCB板会产生损耗&#xff0c;主要包括导体损耗&#xff0c;介电损耗和辐射损耗 导体损耗&#xff1a;导体损耗是由于电流流动过程中产生电阻损耗而发热。 介电损耗&#xff1a;介电损耗是由于电场通过介质时分子的交替极化和晶格碰撞造成的。 辐射损耗&#xff1a;辐…

React 应用实现监控可观测性最佳实践

前言 React 是一个用于构建用户界面的 JavaScript 框架。它采用了虚拟 DOM 和 JSX&#xff0c;提供了一种声明式的、组件化的编程模型&#xff0c;以便更高效地构建用户界面。无论是简单还是复杂的界面&#xff0c;React 都可以胜任。 YApi 是使用 React 编写的高效、易用、功…

报表生成器FastReport .Net用户指南:脚本示例

FastReport的报表生成器&#xff08;无论VCL平台还是.NET平台&#xff09;&#xff0c;跨平台的多语言脚本引擎FastScript&#xff0c;桌面OLAP FastCube&#xff0c;如今都被世界各地的开发者所认可&#xff0c;这些名字被等价于“速度”、“可靠”和“品质”,在美国&#xff…

[Flutter]环境判断

方式一&#xff08;推荐&#xff09; 常量kReleaseMode&#xff0c;它会根据你的应用是以什么模式编译的来获取值。bool.fromEnvironment会从Dart编译时的环境变量中获取值。对于dart.vm.product这个特定的环境变量&#xff0c;它是由Dart VM设置的&#xff0c;用来标明当前是…

基于PaddleNLP的深度学习对文本自动添加标点符号(二)

前言 基于PaddleNLP的深度学习对文本自动添加标点符号的源码版来了&#xff0c;本篇文章主要讲解如何文本自动添加标点符号的原理和相关训练方法&#xff0c;前一篇文章讲解的是使用paddlepaddle已经训练好的一些模型&#xff0c;在一些简单场景下可以通过这些模型进行预测&…

华为防火墙配置指引超详细(包含安全配置部分)以USG6320为例

华为防火墙USG6320 华为防火墙USG6320是一款高性能、高可靠的下一代防火墙,适用于中小型企业、分支机构等场景。该防火墙支持多种安全功能,可以有效抵御网络攻击,保护网络安全。 目录 华为防火墙USG6320 1. 初始配置 2. 安全策略配置 3. 防火墙功能配置 4. 高可用性配…

Git,GitHub,Gitee,GitLab 四者有什么区别?

目录 1. Git 2. GitHub 3. Gitee 4. GitLab 5. 总结概括 1. Git Git 是一个版本管理工具&#xff0c;常应用于本地代码的管理&#xff0c;下载完毕之后&#xff0c;我们可以使用此工具对本地的资料&#xff0c;代码进行版本管理。 下载链接&#xff1a; Git - Downlo…

前端项目在本地localhost可以调取到拍照或麦克风等设备,但是在局域网内IP+端口号访问项目时访问不到设备

前端项目在本地localhost可以调取到拍照或麦克风等设备&#xff0c;但是在局域网内IP端口号访问项目时访问不到设备&#xff0c;调取navigation.mediaDevices时本科可以获取到mediaDevices列表&#xff0c;局域网内ip端口访问时获取不到mediaDevices。 原因&#xff1a; 存在…

vector类(二)

文章目录 vector类的模拟实现1.默认成员变量和函数2.迭代器函数3.空间容量和长度4.[ ]下标调用5.插入操作&#xff08;尾插&#xff09;6.调整容量大小7.判空操作8.删除操作9.插入操作10.size空间大小11.消除操作 vector类的模拟实现 1.默认成员变量和函数 首先自定义构造vec…

uni-app(自定义题色变量)

1.安装sass npm i sass -D 2.安装sass-loader npm i sass-loader10.1.1 -D 3.创建自定义文件 在根目录static目录下&#xff0c;创建scss->_them.scss&#xff0c;目录名称及文件名称自定义即可。 4.定义颜色变量 在_them.scss中&#xff0c;自定义颜色变量&#xff0…

纯分享万岳外卖跑腿系统客户端源码uniapp目录结构示意图

系统买的是商业版&#xff0c;使用非常不错有三端uniapp开源代码&#xff0c;自从上次分享uniapp后有些网友让我分享下各个端的uniapp下的各个目录结构说明 我就截图说以下吧&#xff0c;

鸿蒙OS开发实例:【Web网页】

背景 HarmonyOS平台通过Web控件可支持网页加载展示&#xff0c;Web在中是作为专项参考的。 本篇文章将从Android和iOS平台研发角度出发来实践学习API功能 说明 整个示例是以HarmonyOS开发文档网址作为加载目标页面布局增加了三个按钮“后退”&#xff0c;“前进”&#xff…

Redis、Mysql双写情况下,如何保证数据一致

Redis、Mysql双写情况下&#xff0c;如何保证数据一致 场景谈谈数据一致性三个经典的缓存模式Cache-Aside Pattern读流程写流程 Read-Through/Write-Through&#xff08;读写穿透&#xff09;Write behind &#xff08;异步缓存写入&#xff09; 操作缓存的时候&#xff0c;删除…

Solidity Uniswap V2 Router swapTokensForExactTokens

最初的router合约实现了许多不同的交换方式。我们不会实现所有的方式&#xff0c;但我想向大家展示如何实现倒置交换&#xff1a;用未知量的输入Token交换精确量的输出代币。这是一个有趣的用例&#xff0c;可能并不常用&#xff0c;但仍有可能实现。 GitHub - XuHugo/solidit…

联想 lenovoTab 拯救者平板 Y700 二代_TB320FC原厂ZUI_15.0.677 firmware 线刷包9008固件ROM root方法

联想 lenovoTab 拯救者平板 Y700 二代_TB320FC原厂ZUI_15.0.677 firmware 线刷包9008固件ROM root方法 ro.vendor.config.lgsi.market_name拯救者平板 Y700 ro.vendor.config.lgsi.en.market_nameLegion Tab Y700 #ro.vendor.config.lgsi.short_market_name联想平板 ZUI T # B…

JMM Java内存模型

JMM本身是一个抽象的概念,不是真实存在的,它仅仅是一种规定或者说是规范 1.用来实现线程和主内存直接的抽象关系 2.屏蔽各个硬件平台和操作系统的内存访问差异,使得java程序在各种平台都能达到一致的内存访问效果 JMM的三大特性 可见性 多线程环境下,某个线程修改了变量…

Day55:WEB攻防-XSS跨站CSP策略HttpOnly属性Filter过滤器标签闭合事件触发

目录 XSS跨站-安全防御-CSP XSS跨站-安全防御-HttpOnly XSS跨站-安全防御-XSSFilter(过滤器的意思) 1、无任何过滤 2、实体化 输入框没有 3、全部实体化 利用标签事件 单引号闭合 4、全部实体化 利用标签事件 双引号闭合 5、事件关键字过滤 利用其他标签调用 双引号闭合…