linux-网络编程套接字

linux-网络编程套接字

网络通信是两端通信:
其中一端叫客户端;另-端叫服务端
通信双方中主动发起请求的这一方是客户端-必须知道服务端在哪里
通信双方中被动接收请求的这一方是服务端-- 必须告诉客户端自己在哪里(通常都是一个固定地址)

传输层有两个协议: TCP/UDP 需要用户告诉操作系统,到底传输层应该用哪个

  • UDP (用户数据报协议) :
    无连接,不可靠,面向数据报
    传输性能高,但是不保证可靠传输
    udp适用场景:实时性要求极高,但是安全性要求不是很高的场景—视频传输
  • TCP (传输控制协议):
    面向连接, 可靠传输,面向字节流
    传输性能较低,但是保证可靠传输
    tcp适用场景:安全性要求极高—文件传输

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);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
         socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);
//接收数据(TCP/UDP, 客户端 + 服务器)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
		struct sockaddr *src_addr, socklen_t *addrlen);
//发送数据(TCP/UDP, 客户端 + 服务器)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
		const struct sockaddr *dest_addr, socklen_t addrlen);
基于UDP协议的socket客户端与服务端的通信编程
udp通信流程:
  • 客户端: 1. 创建套接字–> 2. 为套接字绑定地址(客户端不推荐主动绑定) --> 3. 发送数据–> 4.接收数据–> 5.关闭套接字
  • 服务端: 1. 创建套接字–>2.为套接字绑定地址–>3. 接收数据–>4.发送数据–>5.关闭套接字

在这里插入图片描述

使用C++封装一个udpsocket类,来实现socket的简单操作:
 三个文件:

  • udpsocket.hpp 中封装类
  • udp_cli.cpp 实现udp客户端通信
  • udp_srv.cpp 实现udp服务端通信
    udpsocket.hpp
/*=============================================================== 
*   描    述:封装UdpSocket类,实例化对象,向外提供简单的socket接口 
*       1. 创建套接字
*       2. 为套接字绑定地址信息
*       3. 发送数据
*       4. 接收数据
*       5. 关闭套接字
================================================================*/

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

#define CHECK_RET(q) if((q)==false){return -1;}

class UdpSocket{
    private:
        int _sockfd;
    public: 
        bool Socket() {
            //int socket(int domain, int type, int protocol);
            _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
            if (_sockfd < 0) {
                std::cerr << "socket error\n";
                return false;
            }
            return true;
        }
        bool Bind(const std::string &ip, const uint16_t port) {
            //bind(int sockfd, struct sockaddr *addr,socklen_t addrlen)
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            //uint16_t htons(uint16_t hostshort);
            //将主机字节序的16位数据,转换位网络字节序数据返回
            addr.sin_port = htons(9000);
            //192.168.122.132 -> 0xc0a87a84
            //in_addr_t inet_addr(const char *cp);
            //将点分十进制字符串IP地址转换为网络字节序IP地址
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            int ret;//返回值,仅仅用来判断bind是否成功
            socklen_t len = sizeof(struct sockaddr_in);
            ret = bind(_sockfd, (struct sockaddr*)&addr, len);
            if (ret < 0) {
                std::cerr << "bind error\n";
                return false;
            }
            return true;
        }
        bool Send(const std::string &data, const std::string &peer_ip, const uint16_t peer_port) 
									//给对端peer_ip的peer_port端口发送data数据
		{
            //ssize_t sendto(int sockfd, const void *buf, size_t len, 
            //int flags,struct sockaddr *dest_addr, socklen_t addrlen);
            struct sockaddr_in addr;//定义一个结构体struct sockaddr_in addr,
									//然后用要发送到的对端ip地址和对端ip地址中的port端口号
									//分别对addr.sin_family,addr.sin_port,addr.sin_addr.s_addr进行赋值
            addr.sin_family = AF_INET;
            addr.sin_port = htons(peer_port);
            addr.sin_addr.s_addr = inet_addr(peer_ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = sendto(_sockfd, &data[0], data.size(), 0, (struct sockaddr*) &addr, len);
													//通过_sockfd对应的socket结构体中的源ip的源端口
													//将data中data.size()长度的数据发送到对端地址(struct sockaddr*) &addr
													//对端是以(struct sockaddr*) &addr为地址的主机
													//通过最后一个参数len和地址类型struct sockaddr_in addr
													//可以判断出目的ip地址和目的port端口
													//就完成了数据从源ip地址的port端口到目的端ip地址的port端口的发送
            if (ret < 0) {
                std::cerr << "sendto error\n";
                return false;
            }
            return true;
        }
        bool Recv(std::string &buf, std::string &peer_ip, uint16_t &peer_port)
											//Recv中参数均为输出参数
											//定义一个结构体struct sockaddr_in peer_addr来接收该数据是从哪个ip地址的哪个port端口发送过来的
		{
            //ssize_t recvfrom(int sockfd, void *buf, size_t len, 
            //int flags,struct sockaddr *src_addr, socklen_t *addrlen)
            //成功:返回实际接收的数据长度 , 失败:-1
            struct sockaddr_in peer_addr;//定义一个结构体struct sockaddr_in peer_addr来接收该数据是从哪个ip地址的哪个port端口发送过来的
            socklen_t len = sizeof(struct sockaddr_in);
            char tmp[4096] = {0};
            int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
											//从_sockfd中对应的socket结构体的接收队列中,取出一条数据放到tmp中,
											//然后保存发送端的地址到参数[结构体(struct sockaddr*)&peer_addr]中
											//通过对(struct sockaddr*)&peer_addr对端(发送端)地址的解析
											//对三个参数进行输出
											//将对端地址peer_ip和对端端口号peer_port赋值
											//将tmp中的数据放入到buf中
            if (ret < 0) {
                std::cerr << "recvfrom error\n";
                return false;
            }
            //char *inet_ntoa(struct in_addr in);
            //将网络字节序IP地址转换为点分十进制字符串IP地址
            //uint16_t ntohs(uint16_t netshort);
            //将网络字节序的16位数据转换为主机字节序数据
            peer_ip = inet_ntoa(peer_addr.sin_addr);
            peer_port = ntohs(peer_addr.sin_port);
            buf.assign(tmp, ret);
            return true;
        }
        void Close() {
            close(_sockfd);
        }
};

udp_cli.cpp

#include "udpsocket.hpp"
#include <sstream>

int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./udp_cli ip port\n";
        return -1;
    }
    uint16_t port;
    std::string ip = argv[1];
    std::stringstream tmp;
    tmp << argv[2];
    tmp >> port;

    UdpSocket sock;
    CHECK_RET(sock.Socket());
    //客户端不推荐用户主动绑定固定地址,因为一个端口只能被一个进程占用
    //因此一旦端口固定,这个客户端程序就只能启动一个
    while(1) {
        std::string buf;
        std::cin >> buf;
        //当socket还没有绑定地址,这时候操作系统在发送之前可以检测到
        //这时候操作系统会为socket选择一个合适的地址和端口进行绑定
        sock.Send(buf, ip, port);//将buf中的数据通过socket对应的结构体发送到ip主机的port端口

        buf.clear();
        sock.Recv(buf, ip, port);//Recv中的buf,ip和port都是输出参数
								//表示从哪个ip的哪个port端口发送到的数据,存到buf中
        std::cout << "server say:" << buf << std::endl;
    }
    sock.Close();
    return 0;
}

udp_srv.cpp

#include "udpsocket.hpp"
#include <sstream>

int main(int argc, char *argv[]) 
{
    if (argc != 3) {
        std::cerr << "./udp_srv 192.168.122.132 9000\n";
        return -1;
    }
	//用ip和port来接收cin输入的ip和port
    uint16_t port;
    std::string ip = argv[1];
    std::stringstream tmp;
    tmp << argv[2];
    tmp >> port;

    UdpSocket sock;
    CHECK_RET(sock.Socket());
    CHECK_RET(sock.Bind(ip, port));

    while(1) {
        std::string buf;
        std::string peer_ip;
        uint16_t peer_port;
        sock.Recv(buf, peer_ip, peer_port);//Recv中的buf,ip和port都是输出参数,故定义三个变量来接受
											//表示从哪个ip的哪个端口发送到的数据,存到buf中
        std::cout << "client-["<<peer_ip<<":"<<peer_port<<"] say:"
            <<buf<<std::endl;

        buf.clear();
        std::cin >> buf;
        sock.Send(buf, peer_ip, peer_port);//将buf中的数据通过socket对应的结构体发送到peer_ip主机的peer_port端口
    }
    sock.Close();
}

代码运行结果:【注意先启动服务端】
在这里插入图片描述
在这里插入图片描述


基于TCP协议的socket客户端与服务端的通信编程
  • 客户端: 1. 创建套接字–> 2.绑定地址(不推荐主动绑定)–> 3.向服务端发起连接–> 4.发送数据–> 5. 接收数据–> 6.关闭套接字
  • 服务端: 1. 创建套接字–> 2.绑定地址–> 3.开始监听–> 4.获取已完成连接–> 5.通过获取的已完成连接socket接收数据–> 6.通过获取的已完成连接socket发送数据–> 7.关闭套接字

在这里插入图片描述

同样使用C++封装tcpsocket类来实现socket简单操作:

  • tcpsocket.hpp : 封装tcpsocket类
  • tcp_cli.cpp : 实现tcp客户端通信
  • tcp_srv.cpp : 实现tcp服务端通信

tcpsocket.hpp

/*=============================================================== 
*   描    述:封装一个tcpsocket类,向外提供简单的套接字接口 
*       1. 创建套接字
*       2. 为套接字绑定地址信息
*       3. 开始监听
*       4. 向服务端发起连接请求
*       5. 服务端获取新建连接
*       6. 发送数据
*       7. 接收数据
*       8. 关闭套接字
================================================================*/
#include <iostream>
#include <sstream>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define CHECK_RET(q) if((q)==false){return -1;}

class TcpSocket
{
    private:
        int _sockfd;
    public:
        void SetFd(int fd) {
            _sockfd = fd;
        }
        int GetFd() {
            return _sockfd;
        }
        bool Socket() {
            _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//在内核中创建套接字,就是一个struct socket结构体
																//建立进程与网卡之间的联系
																//但是返回值是int,返回的是文件描述符,只是一个套接字操作句柄
            if (_sockfd < 0) {
                std::cerr << "socket error\n";
                return false;
            }
            return true;
        }
        int str2int(const std::string &str){
            int num;
            std::stringstream tmp;
            tmp << str;
            tmp >> num;
            return num;
        }
        bool Bind(const std::string &ip, const std::string &port) {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(str2int(port));
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = bind(_sockfd, (struct sockaddr*)&addr, len);//将_sockfd对应的socket结构体与addr地址绑定,
																  //表示该套接字的socket结构体应该处理特定ip地址中的特定port端口的数据
            if (ret < 0) {
                std::cerr << "bind error\n";
                return false;
            }
            return true;
        }
        bool Listen(const int backlog = 5) {
            //int listen(int sockfd, int backlog);
            //开始监听:通知操作系统,可以开始接收客户端的连接请求了,
            //并且完成三次握手建立连接过程
            //tcp的面向连接,有一个三次握手建立连接过程
            //backlog:客户端最大并发连接数(同一时间最多接收多少个客户端
            //新连接请求)
            int ret = listen(_sockfd, backlog);
            if (ret < 0) {
                std::cerr << "listen error\n";
                return false;
            }
            return true;
        }
        bool Connect(const std::string &srv_ip, const std::string &srv_port)//和bind参数相同,不过bind是绑定(自己)地址,connect是对端地址
		{
            //int connect(int sockfd, sockaddr *addr,socklen_t addrlen)
            //addr: 服务端地址信息
            //addrlen:  地址信息长度
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(str2int(srv_port));
            addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = connect(_sockfd, (struct sockaddr*)&addr, len);//想要建立与对端地址为addr的ip+端口的新连接
																	//_sockfd是创建套接字的返回句柄(int型)
																	//_sockfd这个句柄中包含了(struct sockaddr*)&addr结构体
																	//此地址是自己已经bind绑定的地址
																	//或者是由内核随机分配的地址
																	//而这里参数中的(struct sockaddr*)&addr表示的是想要建立连接的对端地址信息
            if (ret < 0) {
                std::cerr << "connect error\n";
                return false;
            }
            return true;
        }
        bool Accept(TcpSocket &clisock, std::string *ip = NULL, uint16_t *port = NULL) //注意参数
		{
            //int accept(int sockfd, sockaddr *addr, socklen_t *addrlen)
            //sockfd:   监听套接字描述符
            //addr:    客户端地址信息
            //addrlen: 地址信息长度
            //返回值:返回新建连接的socket描述符-与客户端进行数据通信
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(struct sockaddr_in);
			int newfd = accept(_sockfd, (sockaddr*)&cliaddr, &len);//新创建一个套接字来专门和这个cliaddr地址的客户端进行收发数据处理
											//参数(sockaddr*)&cliaddr是输出参数,
											//表示接下来让该_sockfd套接字与该cliaddr地址进行一对一数据收发
			if (newfd < 0) {
                std::cerr << "accept error\n";
                return false;
            }
            clisock.SetFd(newfd);//参数clisock也是一个输出参数,接收新的套接字描述符newfd
								 //保存到新类clisock中,后续操作针对clisock类就相当于操作新的套接字描述符newfd
			
			//当ip,port两个参数传空时,用accept函数中的输出参数里面的地址的ip+port进行赋值即可
            if (ip != NULL) {
                *ip = inet_ntoa(cliaddr.sin_addr);
            }
            if (port != NULL) {
                *port = ntohs(cliaddr.sin_port);
            }
            return true;
        }
        bool Send(std::string &data) {
            //ssize_t send(int sockfd, void *buf, size_t len, int flags)
            //sockfd: 套接字描述符(服务端是新建连接的socket描述符)
            //buf: 要发送的数据
            //len: 要发送的数据长度
            //flags:   0-默认阻塞发送
            //返回值: 成功-返回实际发送的数据长度;失败-返回-1
            int ret = send(_sockfd, &data[0], data.size(), 0);
            if (ret < 0) {
                std::cerr << "send error\n";
                return false;
            }
            return true;
        }
        bool Recv(std::string &buf) {
            //ssize_t recv(int sockfd, void *buf, size_t len, int flags)
            //flags:
            //  0-默认阻塞接收
            //  MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
            //返回值:>0:实际接收的数据长度 ==0:连接断开   <0:错误
            char tmp[4096];
            int ret = recv(_sockfd, tmp, 4096, 0);
            if (ret < 0) {
                std::cerr << "recv error\n";
                return false;
            }else if (ret == 0) {
                std::cerr << "connect shutdown\n";
                return false;
            }
            buf.assign(tmp, ret);
            return true;
        }
        bool Close() {
            close(_sockfd);
        }
};

tcp_cli.cpp

/*=============================================================== 
*   描    述:tcp客户端通信流程
*       1. 创建套接字
*       2. 为套接字绑定地址信息(不推荐用户主动绑定)
*       3. 向服务端发起连接请求
*       4. 发送数据
*       5. 接收数据
*       6. 关闭套接字 
================================================================*/
#include <stdio.h>
#include <signal.h>
#include "tcpsocket.hpp"
void sigcb(int signo)
{
    printf("recv a signo SIGPIPE --- conect shutdown\n");
}
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_cli ip port\n";
        return -1;
    }
    signal(SIGPIPE, sigcb);
    TcpSocket sock;
    /*1. 创建套接字*/
    CHECK_RET(sock.Socket());
    /*2. 为套接字绑定地址信息(不推荐用户主动绑定)*/
    /*3. 向服务端发起连接请求*/
    CHECK_RET(sock.Connect(argv[1], argv[2]));
    while(1) {
        /*4. 发送数据*/
        std::string buf;
        std::cout << "client say: ";
        fflush(stdout);
        std::cin >> buf;
        sock.Send(buf);
        /*5. 接收数据*/
        buf.clear();
        sock.Recv(buf);
        std::cout << "server say: " << buf << std::endl;
    }
    /*6. 关闭套接字 */
    sock.Close();
    return 0;
}

tcp_srv.cpp

/*===============================================================
 *   描    述:tcp服务端通信流程
 *       1. 创建套接字
 *       2. 为套接字绑定地址信息
 *       3. 开始监听
 *       4. 获取已完成连接socket
 *       5. 通过获取的新建socket与客户端进行通信-接收数据
 *       6. 发送数据
 *       7. 关闭套接字 
 ================================================================*/
#include <stdio.h>
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_srv ip port\n";
        return -1;
    }
    TcpSocket lst_sock;
    /*1. 创建套接字*/
    CHECK_RET(lst_sock.Socket());
    /*2. 为套接字绑定地址信息*/
    CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
    /*3. 开始监听*/
    CHECK_RET(lst_sock.Listen());
    while(1){
        /*4. 获取已完成连接socket*/
        TcpSocket clisock;//创建一个新类来接受accept的输出参数,新的套接字描述符对应的类
        bool ret = lst_sock.Accept(clisock);
        if (ret == false) {
            continue;
        }
        /*5. 通过获取的新建socket与客户端进行通信-接收数据*/
        std::string buf;
        ret = clisock.Recv(buf);//后续收发数据的操作都是在类clisock上进行操作
        if (ret == false) {
            clisock.Close();//1.关闭Accept参数中的新类clisock中的套接字
            continue;
        }
        std::cout << "client say: " << buf << std::endl;
        /*6. 发送数据*/
        buf.clear();
        std::cout << "server say: ";
        fflush(stdout);
        std::cin >> buf;
        clisock.Send(buf);
    }
    /*7. 关闭套接字 */
    lst_sock.Close();//2.关闭用来监听连接的套接字
    return 0;
}

运行后发现当前服务端程序只能与一个客户端通信一次
【注意先启动服务端】在这里插入图片描述
在这里插入图片描述
可以看到服务端阻塞:

原因:因为服务端不知道客户端的新连接请求/数据什么时候到来,因此在程序写死的情况下,就会阻塞在recv或者accept两个接口处,导致流程无法继续【代码理解就是封装类中accept以后才收发一次数据,也就是说每次新连接来了之后才能进行一次收发数据,所以这样写,代码有缺陷】
解决方案:服务端为每一个新的客户端都创建一个新的进程/线程来专门与该客户端进行通信。【就是用一个线程/进程负责客户端已完成连接获取功能,然后为每个客户端新建一个线程/进程处理独立通信】

在这里插入图片描述

将tcp服务端的通信改成多进程版本:

tcp_progress.cpp

/*=============================================================== 
*   描    述:tcp服务端通信流程
 *       1. 创建套接字
 *       2. 为套接字绑定地址信息
 *       3. 开始监听
 *       4. 获取已完成连接socket
 *       5. 通过获取的新建socket与客户端进行通信-接收数据
 *       6. 发送数据
 *       7. 关闭套接字 
 ================================================================*/
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"

void sigcb(int no) {
    //如果有僵尸进程可以处理,就一直处理
    //如果没有子进程退出了则waitpid返回0,退出循环
    while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_srv ip port\n";
        return -1;
    }
    signal(SIGCHLD, sigcb);
    TcpSocket lst_sock;
    /*1. 创建套接字*/
    CHECK_RET(lst_sock.Socket());
    /*2. 为套接字绑定地址信息*/
    CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
    /*3. 开始监听*/
    CHECK_RET(lst_sock.Listen());
    while(1){
        /*4. 获取已完成连接socket*/
        TcpSocket clisock;
        bool ret = lst_sock.Accept(clisock);//表示接收到一个新连接
        if (ret == false) {
            continue;
        }
        if (fork() == 0)//接收到一个新连接就创建一个进程来和其收发数据
			{
            while(1) {
                /*5. 通过获取的新建socket与客户端进行通信-接收数据*/
                std::string buf;
                clisock.Recv(buf);
                std::cout << "client say: " << buf << std::endl;
                /*6. 发送数据*/
                buf.clear();
                std::cout << "server say: ";
                fflush(stdout);
                std::cin >> buf;
                clisock.Send(buf);
            }
            clisock.Close();
        }
        clisock.Close();
    }
    /*7. 关闭套接字 */
    lst_sock.Close();
    return 0;
}

或者

将tcp服务端的通信改成多线程版本:

tcp_thread.cpp

/*===============================================================
 *   描    述:tcp服务端通信流程
 *       1. 创建套接字
 *       2. 为套接字绑定地址信息
 *       3. 开始监听
 *       4. 获取已完成连接socket
 *       5. 通过获取的新建socket与客户端进行通信-接收数据
 *       6. 发送数据
 *       7. 关闭套接字 
 ================================================================*/
#include <stdio.h>
#include <pthread.h>
#include "tcpsocket.hpp"

void *thr_start(void  *arg)
{
    TcpSocket *clisock = (TcpSocket*)arg;
    while(1) {
        /*5. 通过获取的新建socket与客户端进行通信-接收数据*/
        std::string buf;
        clisock->Recv(buf);
        std::cout << "client say: " << buf << std::endl;
        /*6. 发送数据*/
        buf.clear();
        std::cout << "server say: ";
        fflush(stdout);
        std::cin >> buf;
        clisock->Send(buf);
    }
    clisock->Close();
    delete clisock;
    return NULL;
}
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cerr << "./tcp_srv ip port\n";
        return -1;
    }
    TcpSocket lst_sock;
    /*1. 创建套接字*/
    CHECK_RET(lst_sock.Socket());
    /*2. 为套接字绑定地址信息*/
    CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
    /*3. 开始监听*/
    CHECK_RET(lst_sock.Listen());
    while(1){
        /*4. 获取已完成连接socket*/
        TcpSocket *clisock = new TcpSocket();
        bool ret = lst_sock.Accept(*clisock);//表示接收到一个新连接
        if (ret == false) {
            continue;
        }
        pthread_t tid;//接收到一个新连接就创建一个线程来和其收发数据
        pthread_create(&tid, NULL, thr_start, (void*)clisock);
        pthread_detach(tid);
    }
    /*7. 关闭套接字 */
    lst_sock.Close();
    return 0;
}

这样客户端和服务端就可以正常通信了:【注意先启动服务端】
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值