【网络】网络编程套接字(一)

网络编程套接字 一

  • 一、网络编程中的一些基础知识
    • 1、认识端口号
    • 2、认识TCP协议和UDP协议
    • 3、网络字节序
  • 二、socket编程
    • 1、sockaddr结构
    • 2、简单的UDP网络程序
      • Ⅰ、服务器的创建
      • Ⅱ、运行服务器
      • Ⅲ、关于客户端的绑定问题
      • Ⅳ、启动客户端
      • Ⅴ、本地测试
      • Ⅵ、网络测试

一、网络编程中的一些基础知识

1、认识端口号

在前面我们说过可以使用IP地址来标识一台主机,但是我们光有IP地址就可以完成通信了嘛?

答案是:不可以,当我们的主机接收到了数据以后还要确定这个数据是发送给哪一个进程的,两台主机的两个软件进行网络通信时,我们还需要有一个其他的标识来区分出这个数据要给哪个程序进行解析,于是就有了端口号。

在这里插入图片描述

端口号(port)是传输层协议的内容,它有以下特点

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

理解 “端口号” 和 “进程ID”

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系? 那在进行网络通信时为什么不直接用PID来代替port呢?

进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。

一台机器上可能会有大量的进程,但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了,而且如果用PID代替端口号,会导致网络管理模块与进程管理模块产生耦合关系,不利于设计出高内聚低耦合的软件。


所以在网络通信中我们可以使用:IP地址+Port号 标识互联网中唯一的一个进程。

此外,从上面通信的例子我们能看出网络通信的本质:其实是进程间通信!,位于不同主机中的两个进程通过网络进行了进程间通信。

2、认识TCP协议和UDP协议

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

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。

  • 传输层协议
  • 有连接,TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。
  • 可靠传输,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。
  • 面向字节流

认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识,后面再详细讨论。

  • 传输层协议
  • 无连接,无需建立连接就可以进行网络传输
  • 不可靠传输,无连接也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,是没有办法进行处理的。
  • 面向数据报

既然UDP协议是不可靠的,那为什么还要有UDP协议的存在?

首先,要保证数据传输的可靠性是需要我们做更多的工作的,TCP协议虽然是一种可靠的传输协议,但这一定意味着TCP协议在底层需要做更多的工作,因此TCP协议底层的实现是比较复杂的

同样的,UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单,UDP协议虽然不可靠,但是它能够快速的将数据发送给对方

编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。

ps: 一些优秀的网站在设计网络通信算法时,会同时采用TCP协议和UDP协议,当网络流畅时就使用UDP协议进行数据传输,而当网络信号差时就使用TCP协议进行数据传输,这样既保证了数据的可靠性又保障了传输的速率。


3、网络字节序

计算机在存储数据时是有大小端的概念的:

  • 大端模式: 数据的高字节内容保存在内存的低地址处。
  • 小端模式: 数据的高字节内容保存在内存的高地址处。

在这里插入图片描述

如果我们编写的程序只在本地机器上运行,那么是不需要考虑大小端问题的,因为同一台机器上的数据采用的存储方式都是一样的,要么采用的都是大端存储模式,要么采用的都是小端存储模式。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的,那么如何定义网络数据流的地址呢?

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
  • 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略直接发送即可;

需要注意的是,所有的大小端的转化工作是由操作系统来完成的,因为该操作属于通信细节,不过也有部分的信息需要我们自行进行处理,比如端口号和IP地址


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

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

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

二、socket编程


socket 是“套接字”的意思,学习 socket 编程,也就是学习计算机之间如何通信,并用编程语言来实现它。

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

1、sockaddr结构

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockaddr结构体,该结构体与sockaddr_insockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族

在这里插入图片描述

此时当我们在传递在传参时,就不用传入sockeaddr_in *sockeaddr_un *这样的结构体,而统一传入sockeaddr *这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。

sockaddr结构体

在这里插入图片描述

sockaddr_in 结构体

在这里插入图片描述

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

2、简单的UDP网络程序

Ⅰ、服务器的创建

UDP服务器的初始化就只需要创建套接字绑定就行了


创建套接字

// 创建 socket 文件描述符 
int socket(int domain, int type, int protocol);

功能:socket函数可以打开一个网络文件,用于网络数据的通信。

对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。

而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

参数说明:

  • domain:创建套接字的域(协议家族),也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:创建套接字时所需的服务类型。如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

返回值说明:

  • 套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。

示例代码:

// udp_server.hpp#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>class UdpServer
{
public:UdpServer(){}void UdpServerInit(){// 1. 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "create socket fail : " << strerror(errno) << std::endl;exit(1);}std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;}~UdpServer(){if (_sockfd > 0){close(_sockfd);}}private:int _sockfd;            // 套接字的文件描述符
};
// udp_server.cpp#include "udp_server.hpp"
#include <iostream>
#include <memory>int main()
{std::unique_ptr<UdpServer> up(new UdpServer());up->UdpServerInit();return 0;
}

在这里插入图片描述


绑定函数

将程序的端口号,IP地址等数据设置进入操作系统内核中

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:要绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。

将点分10进制的ip转换为整数

in_addr_t inet_addr(const char *cp);

功能:该函数可以将主机序列的字符串风格类型的IP, 转换成为网络序列中的整数风格的IP地址。

将整数转换为点分10进制的ip

char *inet_ntoa(struct in_addr in);

功能: 该函数可以将网络序列中的整数风格的IP地址,转换成为主机序列的字符串风格类型的数据。

ps : 这两个函数调用完毕以后不需要再进行网络序列与主机序列的转化了


套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in结构,将对应的网络属性信息填充到该结构当中,然后通过bind函数设置进入操作系统内核当中,由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。

需要注意的是,在发送到网络之前需要将端口号和IP转换为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP。

当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。

// udp_server.hpp#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>enum { SOCKET_ERR = 1, BIND_ERR};// 默认端口号
const static uint16_t default_port = 8080;class UdpServer
{
public:UdpServer(std::string ip, uint16_t port = default_port):_port(port), _ip(ip){std::cout << "ip : " << _ip << " port : " << _port << std::endl;}void UdpServerInit(){// 1. 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "create socket fail : " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;// 2. 填充sockaddr_in结构体struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 将主机序列转换为网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_port = htons(_port);// 3. 绑定IP,端口号if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0){std::cerr << "bind fail :" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind success :" << std::endl;}~UdpServer(){if (_sockfd > 0){close(_sockfd);}}private:int _sockfd;            // 套接字的文件描述符std::string _ip;        // ip地址uint16_t _port;         // 端口号
};
// udp_server.cpp#include "udp_server.hpp"
#include <iostream>
#include <memory>int main()
{std::unique_ptr<UdpServer> up(new UdpServer("1.1.1.1", 8080));up->UdpServerInit();return 0;
}

运行结果,可以看出bind失败了,这与云服务器有关,云服务器不允许我们随意绑定ip,需要让服务器自己指定IP地址。

在这里插入图片描述

当然,云服务器不允许我们随意绑定ip,也有一定的道理,因为对于一款服务器来说,这台设备可能有多个网卡,这台设备可能有多个IP,如果我们只绑定某个特定的IP就会导致只有某个IP能够收到数据,当数据量很大的时候,传输的效率并不是很高,所以我们可以设置IP为INADDR_ANY,设置这个IP表示:绑定本主机上面的所有IP。

在这里插入图片描述

INADDR_ANY的值本质就是0,不存在大小端的问题,因此在设置时可以不进行网络字节序的转换。

在这里插入图片描述

Ⅱ、运行服务器

当服务器初始化完毕后我们就可以启动服务器了,由于服务器是一个永不退出的进程,所以服务器运行以后一定是一个死循环!

读取数据

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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlensrc_addr结构体的长度,返回时此值会被修改为实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

发送数据

 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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

现在服务端通过recvfrom函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为’\0’,此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。

需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP再进行打印输出。

// udp_server.hpp#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>enum
{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR
};class UdpServer
{
public:UdpServer(uint16_t port):_port(port){std::cout << "port : " << _port << std::endl;}void UdpServerInit(){// 1. 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "create socket fail : " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;// 2. 填充sockaddr_in结构体struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 将主机序列转换为网络序列local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(_port);// 3. 绑定IP,端口号if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0){std::cerr << "bind fail :" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind success !" << std::endl;}void UdpServerStart(){// 缓冲区char buf[2048];// 网络信息结构体struct sockaddr_in peer;socklen_t len = sizeof(peer);// 死循环不能让服务器退出while (true){memset(&peer, 0, len);// 收取消息ssize_t num = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);if (num < 0){std::cerr << "recvfrom fail !" << std::endl;continue;}else{// 结尾补上\0,形成C风格字符串buf[num] = '\0';}// 提取客户端的ip和端口号std::string peer_ip = inet_ntoa(peer.sin_addr);uint16_t peer_port = ntohs(peer.sin_port);std::cout << peer_ip << " | " << peer_port << " |# " << buf << std::endl;// 发消息sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer, len);}}~UdpServer(){if (_sockfd > 0){close(_sockfd);}}private:int _sockfd;            // 套接字的文件描述符uint16_t _port;         // 端口号
};

我们服务器启动的时候需要指定端口号,所以这里使用了命令行参数。

// udp_server.cpp#include "udp_server.hpp"
#include <iostream>
#include <memory>// 使用手册
static void usage(std::string proc)
{std::cout << "usage\n\t" << proc << " 端口号" << std::endl;
}// 命令行参数,必须输入两个参数,一个是程序名,一个是端口号
int main(int argc, char* argv[])
{if (argc != 2){usage(argv[0]);exit(USAGE_ERR);}// 提取本地端口号uint16_t port = atoi(argv[1]);std::unique_ptr<UdpServer> up(new UdpServer(port));up->UdpServerInit();up->UdpServerStart();return 0;
}

程序启动以后我们可以使用netstat -naup显示进程的网络信息。

netstat常用选项说明:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -a: 显示所有连接中的接口信息。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

运行结果

在这里插入图片描述

查看网络信息

在这里插入图片描述

netstat命令显示的信息中:

  • Proto表示协议的类型
  • Recv-Q表示网络接收队列
  • Send-Q表示网络发送队列
  • Local Address表示本地地址,
  • Foreign Address表示外部地址
  • State表示当前的状态
  • PID表示该进程的进程ID
  • Program name表示该进程的程序名称。

其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。

Ⅲ、关于客户端的绑定问题

首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要显示的进行IP和端口号的绑定,而客户端不需要显示的进行绑定的,这个绑定的工作由操作系统来进行绑定,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。

服务器是为了给客户提供服务的,因此服务器必须要让客户知道自己的IP地址和端口号,否则客户端是无法向服务端发起请求的,这就是服务端要进行显示绑定的原因,只有一个进程绑定了端口号之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要明确是那个特定的端口号。

一台设备上可以运行很多客户端,例如:B站客户端绑定了8080端口号,那么以后8080端口号就只能给B站客户端使用,如果8080端口号又被淘宝客户端绑定了并且淘宝先启动了,那么B站客户端就无法启动了,因此客户端端口通常是不绑定,由OS动态分配,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。

Ⅳ、启动客户端

客户端的编写与服务端类似,只不过客户端不需要我们进行绑定工作的,此外作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在我们启动客户端时中需要引入服务端的IP地址和端口号。

客户端和服务端在功能上是相互补充的,我们上面的服务器是在读取客户端发来的数据然后回发回去,那么这里我们的客户端就应该向服务端发送数据,然后接收服务器回发的数据。

// client.cpp#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>enum
{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR
};// 使用手册
static void usage(std::string proc)
{std::cout << "usage\n\t" << proc << " IP 端口" << std::endl;
}
// 命令行参数,必须输入三个参数,一个是程序名,一个是IP,一个是端口号
int main(int argc, char* argv[])
{if (argc != 3){usage(argv[0]);exit(USAGE_ERR);}// 1. 得到服务器的IP和端口std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 2. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket fail : " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 3. 填充server结构体struct sockaddr_in server;socklen_t len = sizeof(server);memset(&server, 0, len);server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);// 4. 业务处理std::string message;char buf[2048];while (true){std::cout << "[pan的服务器] :> ";getline(std::cin, message);// 发送消息// 在我们首次调用系统调用发送数据时,OS会随机选择一个端口号 + 自己的IP进行bindssize_t num = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);if (num < 0){std::cerr << "sendto fail !" << std::endl;continue;}struct sockaddr_in temp;socklen_t temp_len = sizeof(temp);memset(&temp, 0, temp_len);// 收消息num = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &temp_len);if (num < 0){std::cerr << "recvfrom fail !" << std::endl;continue;}else{buf[num] = '\0';}std::cout << "server's message | " << buf << std::endl;}return 0;
}

Ⅴ、本地测试

现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1地址,服务端的端口号就是8080。

  • 127.0.0.1 :本地环回,表示当前主机的地址,通常用来进行本地通信或测试。

我们要让服务端先运行,然后再让客户端运行,之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出后回发,这时我们在服务端和客户端的窗口都能看到我们输入的内容。
在这里插入图片描述

此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是44777。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

在这里插入图片描述

Ⅵ、网络测试

如果你是云服务器,请确保你想使用的端口已经开放,下面是腾讯云的云服务器开放端口的方法:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

好了,我们开始进行网络测试:

在这里插入图片描述

你可以将此客户端软件给更多的人,让它们都能够连接你的服务器,进行网络通信。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【试题002】C语言有关于sizeof的使用

1.说明&#xff1a;sizeof()是测量数据类型所占用的内存字节数&#xff0c;字符串常量在存储时除了要存储有效字节外&#xff0c;还要存储一个字符串结束志‘\0’。 2.代码举栗子&#xff1a; #include <stdio.h> int main() {char str[] "book";printf(&qu…

Jupyter Notebook 设置黑色背景主题

Jupyter Notebook 设置黑色背景主题 # 包安装 pip install jupyterthemes -i https://mirrors.aliyun.com/pypi/simple pip install --upgrade jupyterthemes # 查看可用主题 jt -l # monokai暗背景&#xff0c;-f(字体) -fs(字体大小) -cellw(占屏比或宽度) -ofs(输出段的字…

黑马JVM总结(三十七)

&#xff08;1&#xff09;synchronized-轻量级锁-无竞争 &#xff08;2&#xff09;synchronized-轻量级锁-锁膨胀 重量级锁就是我们前面介绍过的Monitor enter &#xff08;3&#xff09;synchronized-重量级锁-自旋 &#xff08;4&#xff09;synchronized-偏向锁 轻量级锁…

中科芯与IAR共建生态合作,IAR集成开发环境全面支持CKS32系列MCU

中国上海–2023年10月18日–嵌入式开发软件和服务的全球领导者IAR今日宣布&#xff0c;与中科芯集成电路有限公司&#xff08;以下简称中科芯&#xff09;达成生态合作&#xff0c;IAR已全面支持CKS32系列MCU的应用开发。这一合作将进一步推动嵌入式系统的发展&#xff0c;并为…

CSS魔法!如何将任意CSS类型转换为数值?

在 CSS 中有各式各样的类型值&#xff0c;例如 1rem、10vw、100cqw等等&#xff0c;这些相对值给与了 CSS 强大的适应能力。但有时候&#xff0c;我们还需要知道这些相对值所对应的真实值&#xff0c;也就是px值&#xff0c;比如在移动端&#xff0c;我们经常会设置这样的根字号…

简单了解一下:Node全局对象和事件监听和触发

Node的全局对象&#xff0c;就是整个应用都能引用的对象。Node本身内置了多个全局变量、全局对象和全局函数。 全局变量有两个&#xff1a;__filename和__dirname&#xff0c; __filename&#xff1a;表示当前正在执行的脚本文件名&#xff1b; __dirname&#xff1a;当前脚本…

【公益案例展】广碳所——恒生电子基于区块链技术打造区域性碳中和登记系统...

‍ 恒生电子公益案例 本项目案例由恒生电子投递并参与数据猿与上海大数据联盟联合推出的 #榜样的力量# 《2023中国数据智能产业最具社会责任感企业》榜单/奖项”评选。 ‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 全球气候变暖、温室效应明显、二氧化碳排放增多&#…

CSS之Flex布局的详细解析

Flex布局 目标&#xff1a;熟练使用 Flex 完成结构化布局 01-标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如&#xff1a;块元素独占一行&#xff0c;行内元素可以一行显示多个。 02-浮动 基本使用 作用&#xff1a;让块元素水平排列…

Excel提高工作效率常用功能

定位快捷键使用 CtrlG或者F5 根据不同类别插入空行 例&#xff1a;以下表&#xff0c;以部门为单位&#xff0c;每个部门后插入空白行 部门姓名出勤基本工资岗位津贴公体加班绩效基数工龄应发合计财务部姓名73115002101710财务部姓名11116006003401502363财务部姓名5271000…

新年学新语言Go之四

一、前言 任何编程语言都有类型系统&#xff0c;类型系统解决了数据的存取问题&#xff0c;它决定了使用这个类型需要开辟内存空间大小以及数据是如何存放的&#xff0c;也解决如何读出数据&#xff0c;因为在内存中相同二进制值不同类型的含义是不一样的&#xff0c;关于Go基…

微信小程序仿苹果负一屏由弱到强的高斯模糊

进入下面小程序可以体验效果&#xff0c;然后进入更多。查看模糊效果 一、创建小程序组件 二、代码 wxml: <view class"topBar-15"></view> <view class"topBar-14"></view> <view class"topBar-13"></view&…

【C++】C++的类型转换

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C学习 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C】C智…

微信小程序的OA会议之首页搭建

目录 一.小程序的布局 1.1. flex是什么 1.2. flex布局 1.3.总体布局 二.轮播图 2.1. 组件 2.2. 数据请求 2.3. 页面 三.首页 2.1. 视图 2.2.数据 2.3. 样式 好啦今天就到这里了&#xff0c;希望能帮到你哦&#xff01;&#xff01;&#xff01; 一.小程序的布局 …

基于 Debian 稳定分支发行版的Zephix 7 发布

导读Zephix 是一个基于 Debian 稳定版的实时 Linux 操作系统。它可以完全从可移动媒介上运行&#xff0c;而不触及用户系统磁盘上存储的任何文件。 Zephix 是一个基于 Debian 稳定版的实时 Linux 操作系统。它可以完全从可移动媒介上运行&#xff0c;而不触及用户系统磁盘上存…

PHP-FFMpeg 操作音视频

✨ 目录 &#x1f388; 安装PHP-FFMpeg&#x1f388; 视频中提取一张图片&#x1f388; 视频中提取多张图片&#x1f388; 调整视频大小&#x1f388; 视频添加水印&#x1f388; 生成音频波形&#x1f388; 音频转换&#x1f388; 给音频添加元数据&#x1f388; 拼接多个音视…

微前端四:qiankun在开发中遇到的问题

在qiankun开发中会遇到很多问题&#xff0c;上一篇微前端三&#xff1a;qiankun 协作开发和上线部署其实也是在解决一些经常遇到的问题&#xff0c;下面的两点也算是比较经典的了 1、子应用图片路径问题 2、基座是Vue2.0 element ui 配合 子应用 Vue3.0 element plus 导致的样…

Linux-git

文章目录 git简介git常用命令配置初始化仓库将文件添加到暂存区将暂存区文件加入版本库对比工作区某文件和暂存区中的区别将暂存区的文件移除但git仍管理将文件移除暂存区并且git不再管理查看版本库切换到之前的版本恢复文件持久化 云端将本地的项目推送到远程仓库将远程仓库的…

weapp-tailwindcss for uni-app 样式条件编译语法插件

weapp-tailwindcss for uni-app 样式条件编译语法插件 版本需求 2.10.0 weapp-tailwindcss for uni-app 样式条件编译语法插件 这是什么玩意?如何使用 tailwind.config.js 注册postcss 插件注册 uni-app vite vue3uni-app vue2 配置完成 配置项 这是什么玩意? 在 uni-app …

[1Panel]开源,现代化,新一代的 Linux 服务器运维管理面板

测评介绍 本期测评试用一下1Panel这款面板。1Panel是国内飞致云旗下开源产品。整个界面简洁清爽&#xff0c;后端使用GO开发&#xff0c;前端使用VUE的Element-Plus作为UI框架&#xff0c;整个面板的管理都是基于docker的&#xff0c;想法很先进。官方还提供了视频的使用教程&…

vue使用pdf-dist实现pdf预览以及水印

vue使用pdf-dist实现pdf预览以及水印 一.使用pdf-dist插件将PDF文件转换为一张张canvas图片 npm install pdf-dist二.页面引入插件 const pdfJS require("pdfjs-dist"); pdfJS.GlobalWorkerOptions.workerSrc require("pdfjs-dist/build/pdf.worker.entry&…