网络编程套接字(2): 简单的UDP网络程序

news2024/9/28 19:22:09

文章目录

  • 网络编程套接字(2): 简单的UDP网络程序
    • 3. 简单的UDP网络程序
      • 3.1 服务端创建
        • (1) 创建套接字
        • (2) 绑定端口号
        • (3) sockaddr_in结构体
        • (4) 数据的接收与发送
          • 接收
          • 发送
      • 3.2 客户端创建
      • 3.3 代码编写
        • (1) v1_简单发送消息
        • (2) v2_小写转大写
        • (3) v3_模拟命令行解释器
        • (4) v4_多线程版本的群聊系统
        • (5) v5_Windows与Linux配合聊天室

网络编程套接字(2): 简单的UDP网络程序

3. 简单的UDP网络程序

3.1 服务端创建

(1) 创建套接字

在这里插入图片描述

create an endpoint for communication: 创建用于通信的端点

头文件:
        #include <sys/types.h>         
        #include <sys/socket.h>

函数原型:
        int socket(int domain, int type, int protocol);

参数说明:
		第一个参数domain:   指定套接字的通信域
        第二个参数type:     指定套接字的服务类型(套接字的种类) 
        第三个参数protocol: 代表创建套接字的协议(默认为0),0,系统会自动判断是tcp还是udp

返回值: 
	    套接字创建成功: 返回一个文件描述符
        套接字创建失败: 返回-1, 并且设置错误码

关于socket参数详细介绍:

(1) domain: 指定套接字的通信域,相当于 struct sockaddr结构体的前16比特位(2字节)

在这里插入图片描述

domain的选项是以宏的形式给出的,我们直接选用即可。常用就是上面框住的两个:

  • AF_UNIX,本地通信
  • AF_INET(IPv4)或者 AF_INET6(IPv6),网络通信

(2) type: 指定套接字的服务类型

在这里插入图片描述

该参数的选项也是像domain一样以宏的形式给出,直接选用。常用的是上面两个:

  • SOCK_STREAM: 基于TCP的网络通信,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

  • SOCK_DGRAM: 基于UDP的网络通信,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)

(2) 绑定端口号

在这里插入图片描述

bind a name to socket:将名称绑定到套接字

头文件:
       #include <sys/types.h>          
       #include <sys/socket.h>

函数原型:
       int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);

参数说明:
	   第一个参数sockfd:  文件描述符, 即要绑定的套接字
	   第二个参数addr:    网络相关的结构体, 包含IP地址、端口号等
	   第三个参数addrlen: 传入结构体addr(第二个参数)的实际长度大小
           
返回值:
	   绑定成功: 返回0
	   绑定失败: 返回-1,并且设置错误码

参数addr的类型是:struct sockaddr *,也就是如图的结构体

在这里插入图片描述

我们需要做的就是:定义一个 sockaddr_in 的结构体,即上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换

(3) sockaddr_in结构体

在这里插入图片描述

  • __SOCKADDR_COMMON是一个宏

在这里插入图片描述

#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

sa_prefix:代表外面传入的参数sin_

sa_prefix##family:##代表合并拼接,意思是sa_prefix与family合并拼接

sa_prefix就是sin_,则sa_prefix##family表示sin_family

sa_family_t:代表16位整数

在这里插入图片描述

就是这16位地址类型

在这里插入图片描述

  • sin_port: 是当前服务器需要绑定的端口号,它的类型是 in_port_t,代表16位的整数

在这里插入图片描述

  • sin_addr: 代表IP地址,它的类型是一个in_addr的结构体,它里面的内容是32位的整数

在这里插入图片描述

  1. 关于这个IP地址:我们要传入字符串风格的,但是这里需要4字节整数风格,所以需要转化,比如"1.1.1.1"-> uint32_t,问:能不能强转呢?
    不能强转, 强转只能改变类型, 不改变二进制构成

  2. 我们转化完了还是本主机的4字节序列,需要网络序列,所以要将主机序列转化成为网络序列

上面的2步用 inet_addr函数就可以完成

在这里插入图片描述

  1. 但是我们的云服务器,或者一款服务器,一般不要指明某一个确定的IP

所以这里的ip地址我们填 INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定

  • sin_zero: 表示该结构体的填充字段(即上面讲的sin_family,sin_port,sin_addr.s_add)

总结: 未来使用这个函数时,需要所以填充:sin_family,sin_port,sin_addr.s_addr这3个字段,因为不关注其他字段,所以在填充之前需要对该结构体清空,我们可以采用 memset或 bzero函数来完成。

bind的作用:

上面如果我们只设置了sockaddr_in这个结构体,它只是在用户空间的特定函数栈帧上,不在内核中,所以bind的作用就是把文件字段进行绑定关联,这样这个文件就是网络文件

(4) 数据的接收与发送

接收

在这里插入图片描述

receive a message from a socket:从套接字接收消息

头文件: 
        #include <sys/types.h>
        #include <sys/socket.h>

函数原型:
		 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr 							*src_addr, socklen_t *addrlen);

参数说明:
	     第一个参数sockfd: 文件描述符,就是上面我们绑定好的套接字
    	 第二个参数buf: 接收数据的缓冲区(自己定义)
         第三参数len:   缓冲区的长度
         第四个参数flags: 读取方式,默认设为0表示阻塞式读取
         第五个参数src_addr: (输入)对应套接字的接收缓冲区
         第六个参数addrlen:(输出)src_addr结构体的长度

返回值:
         成功: 返回实际读到的字节数
         失败: 失败返回-1,并设置错误码

socklen_t 是一个32位的无符号整数

  • 参数src_addr与addrlen:输入输出型参数

  • src_addr: 输入时传入对应套接字的接收缓冲区,输出时包含客户端的ip和port

  • addrlen: 输入时传入对应套接字的接收缓冲区,输出时表示实际输出的结构体大小

我们做的是定义一个 sockaddr_in 的结构体,把结构体传给参数src_addr,需要强制类型转换

发送

在这里插入图片描述

send a message on a socket: 在套接字上发送消息

头文件:
		   #include <sys/types.h>
           #include <sys/socket.h>

函数原型:
		   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                           const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
			
           第一个参数sockfd: 文件描述符,从哪个套接字去发送消息
           第二个参数buf: 发送数据的缓冲区
           第三参数len: 要发送的数据长度
           第四个参数flags:发送方式,默认设为0表示阻塞式发送
           第五个参数dest_addr:下面解释
           第六个参数addrlen:dest_addr结构体的长度

返回值:
		   成功: 实际发送的字节数
		   失败: 失败返回-1,并设置错误码
  • dest_addr和addrlen 是一个输入型参数

  • dest_addr:指向目的地址的结构体指针,表示要发给谁

  • addrlen:表示目的地址结构体的长度

我们做的是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体传给dest_addr**,需要强制类型转换**

3.2 客户端创建

还是3步:创建套接字,bind(不需要自己绑定,由OS自动分配),处理数据接收与发送

3.3 代码编写

这里一共提供5个版本的udp代码

err.hpp:这个代码是公用的后续不在给出

#pragma once
enum
{
    USAGE_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
};

(1) v1_简单发送消息

客户端向服务端发送消息,服务端收到后再把消息发回给客户端

udp_server.hpp

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include"err.hpp"
using namespace std;


namespace ns_server
{

    const static uint16_t default_port=8080;

    class Udpserver
    {
    public:
        Udpserver(uint16_t port=default_port)
            :port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }

        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;          // 远端
                socklen_t len = sizeof(peer);     // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,buffer,strlen(buffer),0,(struct sockaddr*)&peer, 		  	                        sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
    };
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_client.cc

#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的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());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "please Enter# ";
        cin>>message;
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));

        // 把消息再收回来(回显回来)

        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len =sizeof(temp);
        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }
    }
}

运行结果:

先运行服务端,再启动客户端,客户端先用本地环回进行测试,测试成功

在这里插入图片描述

运行程序后看到套接字是创建成功的,对应得到到的文件描述符是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被使用用的文件描述符就是3

(2) v2_小写转大写

v2在v1版本的基础增加了业务处理,上层使用了回调函数实现大小写转换

udp_server.hpp

#pragma once

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;


namespace ns_server
{

    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

    class Udpserver
    {
    public:
        Udpserver(func_t cb, uint16_t port=default_port)
            :service_(cb)
            ,port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }

        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;


                // 做业务处理
                string response=service_(buffer);

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
    };
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可

// 这里是小写转大写
string transactionString(string request)   // request就是一个字符串
{
    string ret;
    char c;
    for(auto&r:request)
    {
        if(islower(r))
        {
            c=toupper(r);
            ret.push_back(c);
        }
        else
        {
            ret.push_back(r);
        }
    }
    return ret;
}
int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(transactionString,port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_client.cc

#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        		serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的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());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "please Enter# ";
        cin>>message;
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));

        // 把消息再收回来(回显回来)

        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len =sizeof(temp);
        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }
    }
}

运行结果:

在这里插入图片描述

(3) v3_模拟命令行解释器

v3是在v2原有的业务处理下修改了功能,只要我们在客户端输入命令服务端就会返回运行结果,popen函数可以实现简单的命令行解释

udp_server.hpp

#pragma once

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;

namespace ns_server
{
    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

    class Udpserver
    {
    public:
        Udpserver(func_t cb, uint16_t port=default_port)
            :service_(cb)
            ,port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }


        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;

                // 做业务处理
                string response=service_(buffer);

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
    };
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
static bool isPass(string &command)
{
    auto pos=command.find("rm");
    if(pos!=string::npos) return false;
    pos=command.find("mv");
    if(pos!=string::npos) return false;
    pos=command.find("while");
    if(pos!=string::npos) return false;
    pos=command.find("kill");
    if(pos!=string::npos) return false;

    return true;
}

// 让同学们, 在你的本地把命令给我, server再把结果给你!
string excuteCommand(string command)   // command就是一个命令
{
    // 1. 安全检查
    if(!isPass(command))
        return "you are a bad man";

    // 2. 业务逻辑处理
    FILE*fp=popen(command.c_str(),"r");
    if(fp==nullptr)
        return "None";

    // 3. 获取结果
    char line[1024];
    string ret;
    while(fgets(line,sizeof(line),fp)!=NULL)
    {
        ret+=line;
    }
    pclose(fp);

    return ret;
}

int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(excuteCommand,port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_server.cc

#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;


// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的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());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "[遇健的服务器]# ";
        getline(cin,message);
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建            发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                       sizeof(server));

        // 把消息再收回来(回显回来)

        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len =sizeof(temp);
        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }

    }
}

运行结果:

在这里插入图片描述

(4) v4_多线程版本的群聊系统

v4在v3的基础上加入了之前写的生产消费者模型,多线程实现了群聊系统

LockGuard.hpp

#include<iostream>
#include<pthread.h>
using namespace std;

class Mutex   //自己不维护锁,由外部传入
{
public:
    Mutex(pthread_mutex_t* mutex)
        :_pmutex(mutex)
    {

    }

    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }

    ~Mutex()
    {}

private:
    pthread_mutex_t* _pmutex;   //锁的指针
};

class LockGuard  //自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t* mutex)
        :_mutex(mutex)
    {
        _mutex.lock();
    }

    ~LockGuard()
    {
        _mutex.unlock();
    }

private:
    Mutex _mutex;   //锁的指针
};

RingQueue.hpp

#include<iostream>
#include<pthread.h>
#include<vector>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<semaphore.h>
#include<mutex>
using namespace std;

// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源
static const int N=5;

template<class T>
class RingQueue
{
private:

    void P(sem_t &s)
    {
        sem_wait(&s);
    }

    void V(sem_t &s)
    {
        sem_post(&s);
    }

    void Lock(pthread_mutex_t &m)
    {
        pthread_mutex_lock(&m);
    }

    void Unlock(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }

public:
    RingQueue(int num=N)
        :_ring(num)
        ,_cap(num)
    {
        sem_init(&_data_sem,0,0);
        sem_init(&_space_sem,0,num);
        _c_step=_p_step=0;

        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);
    }

    void push(const T&in)     // 对应生产者
    {
        // 1.信号量的好处:
        // 可以不用在临界区内部做判断, 就可以知道临界资源的使用情况

        // 2.什么时候用锁, 什么时候用sem? --- 你对应的临界资源, 是否被整体使用!

        // 生产 --- 先要申请信号量
        // 信号量申请成功 - 则一定能访问临界资源
        P(_space_sem);
        Lock(_p_mutex);
        // 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢
        _ring[_p_step++]=in;
        _p_step%=_cap;
        V(_data_sem);
        Unlock(_p_mutex);
    }

    void pop(T*out)           // 对应消费者
    {
        // 消费
        P(_data_sem);    // 1.   先申请信号量是为了更高效
        Lock(_c_mutex);  // 2. 
        *out=_ring[_c_step++];
        _c_step%=_cap;
        V(_space_sem);
        Unlock(_c_mutex);
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    vector<T> _ring;
    int _cap;           // 环形队列容器大小
    sem_t _data_sem;    // 只有消费者关心
    sem_t _space_sem;   // 只有生产者关心
    int _c_step;        // 消费位置
    int _p_step;        // 生产位置

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

udp_server.hpp

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
#include<unordered_map>
#include"ringQueue1.hpp"
#include"lockGuard.hpp"
#include"thread.hpp"
using namespace std;


// 群聊系统 --- 一个线程收消息, 一个线程发消息
// 标识一个客户端: ip+port , 使用unordered_map构建<ip+port, 客户端套接字>来表示某个用户发的消息

namespace ns_server
{

    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

    class Udpserver
    {
    public:
        Udpserver(uint16_t port=default_port)
            :port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
            pthread_mutex_init(&_lock,nullptr);

            p=new Thread(1,bind(&Udpserver::Recv,this));
            c=new Thread(2,bind(&Udpserver::Broadcast,this));
        }

        void StartServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

            p->run();
            c->run();
        }

        void addUser(const string&name,const struct sockaddr_in&peer)
        {
            // online[name]=peer
            LockGuard lockguard(&_lock);
            auto iter=onlineuser.find(name);
            if(iter!=onlineuser.end())        // 存在(找到了)直接返回
                return;

            onlineuser.insert(make_pair(name,peer));   // 不存在(没找到)就插入
        }

        void Recv()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;


                // 构建一个用户, 并检查
                string name=clientip;
                name+="-";
                name+=to_string(clientport);

                 // 构建哈希表来存储用户 - 如果不存在,就插入;如果存在,什么都不做
                addUser(name,peer);  

                string message=name+">>"+buffer;  

                _rq.push(message);    // 消息放入环形队列中
            }
        }


        // 发消息  --- 给所有在线用户
        void Broadcast()
        {
            while(true)
            {
                string sendstring;
                _rq.pop(&sendstring);    // 从环形队列中读到了消息   

                vector<struct sockaddr_in> v;   // 把需要发送的信息放到(拷贝)一个数组中<这是内存级的拷贝>
                {
                    LockGuard lockguard(&_lock);
                    for (auto user:onlineuser)
                    {
                        v.push_back(user.second);
                    }
                }
                for(auto user: v)
                {
                    sendto(sock_,sendstring.c_str(),sendstring.size(),0,(struct sockaddr*)&user,sizeof(user));
                    cout<<"send done ..."<<sendstring<<endl;
                }
            }
        }

        ~Udpserver()
        {
            pthread_mutex_destroy(&_lock);
            p->join();
            c->join();

            delete p;
            delete c;
        }

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        unordered_map<string, struct sockaddr_in> onlineuser;   // 保存在线用户 --- 需要加锁保证安全
        pthread_mutex_t _lock;
        RingQueue<string> _rq;
        Thread*p;
        Thread*c;
    };
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可

int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(port));

    usvr->StartServer();

    return 0;
}

udp_client.cc

#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}


// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的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());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "[遇健的服务器]# ";
        getline(cin,message);
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));

        // 把消息再收回来(回显回来)

        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len =sizeof(temp);
        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }

    }
}

运行结果:

在这里插入图片描述

在这里插入图片描述

(5) v5_Windows与Linux配合聊天室

我们可以以Linux云服务器作为服务端,Windows作为客户端,在Windows下我们要修改成Windows下的接口,同时开放云服务器的端口号,使用v4版本的服务端代码

Windows下的客户端

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<WinSock2.h>
#include<string>
#include<cstring>
using namespace std;

#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")

uint16_t serverport = 8080;
std::string serverip = "47.108.235.67";

//std::string serverip = "127.0.0.1";

int main()
{
    WSADATA WSAData;

    if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
    {
        cerr << "init error" << endl;
        return -1;
    }

    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(-2);
    }

    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());
   
    // 3. 向服务器发送消息(这里暂时由用户充当)
    while (true)
    {
        // 用户输入
        string message;
        cout << "Please Enter Your Message# ";
        getline(cin, message);

        // 发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server,                      sizeof(server));

        char buffer[2048];
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp,                           &len);
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << buffer << endl;     // 往1号文件描述符输出
        }
    }
    closesocket(sock);
    WSACleanup();

    return 0;
}

运行结果:

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件测试 day3

今天目标 执行用例&#xff08;课上案例编写的用例&#xff09; 缺陷相关知识 能够说出软件缺陷判定标准 能够说出项目中缺陷的管理流程 能够使用Excel对于缺陷进行管理 能使用工具管理缺陷一、用例执行 说明&#xff1a;执行结果与用例的期望结果不一致&#xff08;含义&…

Lingo软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Lingo是一款专门为解决线性和非线性优化问题而设计的专业软件&#xff0c;广泛应用于运筹学、工程管理、交通管理、生产调度、物流管理等领域。它提供了一个易于使用的界面和灵活的求解器&#xff0c;能够高效地求解大规模的线性…

前端需要理解的Vue知识

1 模板语法 Vue使用基于 HTML 的模板语法&#xff0c;能声明式地将其组件实例的数据绑定到DOM。所有Vue 模板可以被符合规范的浏览器和 HTML 解析器解析。Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统&#xff0c;当应用状态变更时&#xff0c;Vue 能够智能…

Android 实现资源国际化

前言 国际化指的是当Android系统切换语言时&#xff0c;相关设置也随之改变&#xff0c;从而使用不同的国家地区&#xff1b; 简而言之&#xff0c;就是我们的Android App中的文字和图片会随着不同国家的地区变化从而切换为不同语言文字和不同国家的图片 文字图片国际化 只要…

Python绘图系统9:新建绘图类型控件,实现混合类型图表

文章目录 绘图类型控件改造AxisList更改绘图逻辑源代码 Python绘图系统&#xff1a; 从0开始实现一个三维绘图系统自定义控件&#xff1a;坐标设置控件&#x1f4c9;坐标列表控件&#x1f4c9;支持多组数据的绘图系统图表类型和风格&#xff1a;散点图和条形图&#x1f4ca;混…

【已解决】在 SpringBoot 中使用 CloseableHttpClient 调用接口时,接收参数中的中文变为“?“

问题描述 由于项目需要&#xff0c;需要在代码中使用POST请求去调用另一个服务的接口&#xff0c;即不通过前端&#xff0c;A 项目直接在方法中发起HTTP请求调用 B 项目的接口&#xff0c;当请求体中的参数有中文时&#xff0c;参数接收后中文会变为“?”。 具体原因是参数的…

玩转git第7章节,本地git的用户名和密码的修改

一 本地git的用户名和密码 1.1 本地用户名和密码修改 1.本地用户名修改 2.凭据管理 3.进行修改密码 1.2 代码提交操作

8、Spring_整合Mybatis

五、Spring整合Mybatis 1.添加依赖 添加依赖 <dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.17.RELEASE</version></dependency><depend…

Nuxt.js--》添加路由、视图和过渡效果

博主今天开设Nuxt.js专栏&#xff0c;带您深入探索 Nuxt.js 的精髓&#xff0c;学习如何利用其强大功能构建出色的前端应用程序。我们将探讨其核心特点、灵活的路由系统、优化技巧以及常见问题的解决方案。无论您是想了解 Nuxt.js 的基础知识&#xff0c;还是希望掌握进阶技巧&…

LibreOffice新一代的办公软件for Mac/Windows免费版

LibreOffice是一款免费、开源的办公软件套件&#xff0c;可在多个操作系统上运行&#xff0c;包括Windows、Mac和Linux。它提供了一系列功能强大的办公工具&#xff0c;包括文档处理、电子表格、演示文稿、数据库管理等。 LibreOffice的界面简洁直观&#xff0c;与其他流行的办…

mybatis与spring集成与spring aop集成pagehelper插件

Mybatis与Spring的集成 Mybatis是一款轻量级的ORM框架&#xff0c;而Spring是一个全栈式的框架&#xff0c;二者的结合可以让我们更加高效地进行数据持久化操作。 Mybatis与Spring的集成主要有两种方式&#xff1a;使用Spring的Mybatis支持和使用Mybatis的Spring支持。 使用…

MySQL学习-day1

2023.8.26 今天是8月26号&#xff0c;正式开始学习MySQL&#xff0c;看的是尚硅谷的MySQL教程&#xff0c;每天记录一下自己的学习笔记&#xff0c;以供复盘。 数据库的常见概念 1、DB&#xff1a;数据库&#xff0c;存储数据的容器。 2、DBMS&#xff1a;数据…

kettle实现爬虫

步骤概览 获取请求 请求地址 东方财富网股票请求 自定义常量数据 获取HTTP请求之前&#xff0c;必须先定义一个URL常量作为HTTP client的输入 HTTP client 注&#xff1a;此处得到的数据并不是原生的json字符串&#xff0c;自己可以用文本文件输出测试以下。如下图 JavaScri…

Python实现自动登录+获取数据

前言 Dy这个东西想必大家都用过&#xff0c;而且还经常刷&#xff0c;今天就来用代码&#xff0c;获取它的视频数据 环境使用 Python 3.8 Pycharm 模块使用 requests selenium json re 一. 数据来源分析 1. 明确需求 明确采集网站以及数据内容 网址: https://www.dy.co…

PyCharm软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 PyCharm是一种集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门为Python开发者设计。它是由捷克软件公司JetBrains开发的&#xff0c;为Python开发人员提供了高效、易用和功能丰富的工具集。 以下是PyCharm软件的主要…

<JDBC>

文章目录 1.JDBC核心技术1.数据的持久化2.JAVA中的数据存储技术3.JDBC介绍4.JDBC体系结构5.JDBC程序编写步骤 2.获取数据库连接1.Driver接口实现类2.注册与加载JDBC驱动3.URL4.用户和密码 3. PreparedStatement 和 Statement1.PreparedStatement介绍2. PreparedStatement vs St…

pytorch学习(8)——现有网络模型的使用以及修改

1 vgg16模型 1.1 vgg16模型的下载 采用torchvision中的vgg16模型&#xff0c;能够实现1000个类型的图像分类&#xff0c;VGG模型在AlexNet的基础上使用3*3小卷积核&#xff0c;增加网络深度&#xff0c;具有很好的泛化能力。 首先下载vgg16模型&#xff0c;python代码如下&…

Ae 效果:CC Jaws

过渡/CC Jaws Transition/CC Jaws CC Jaws&#xff08;CC 锯齿&#xff09;效果为视频或图像创造独特的锯齿状过渡效果。它允许用户控制中心点、方向、高度、宽度和形状&#xff0c;从而提供多种独特的过渡样式。 ◆ ◆ ◆ 效果属性说明 Completion 完成度 控制过渡效果的完成…

【MYSQL】排序时 如何将0排到最后,并让其他值按正序展示?

背景&#xff1a;展示排名时需要1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;这样展示但是有些没有排名得数据字段默认值时0&#xff0c;这时直接用ASC就会出现问题 实现效果 实现方式&#xff1a;使用MySQL的ORDER BY语句来实现。以下是一个示例的SQL查…

Creo软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Creo是一款机械设计软件&#xff0c;全称为Creo Parametric&#xff0c;是PTC公司推出的一款三维CAD/CAM/CAE软件。Creo被广泛应用于机械设计、汽车、航空、航天、电子、家电、玩具等各个行业&#xff0c;是世界上应用最广泛、最…