当前位置: 首页 > news >正文

违章建设举报网站南昌网站定制开发公司

违章建设举报网站,南昌网站定制开发公司,半天班3500急招店员,wordpress ctf目录 一、UDP中的socket编程常用接口 1. socket的含义 2. sockaddr结构 3. socket编程中UDP协议常用接口介绍 3.1 创建socket文件描述符#xff08;TCP/UDP、客户端 服务器#xff09; 3.2 绑定端口号#xff08;TCP/UDP#xff0c;服务器#xff09; 3.3 接收数据…目录 一、UDP中的socket编程常用接口 1. socket的含义 2. sockaddr结构 3. socket编程中UDP协议常用接口介绍 3.1 创建socket文件描述符TCP/UDP、客户端 服务器 3.2 绑定端口号TCP/UDP服务器 3.3 接收数据 3.4 发送数据给远端服务器 4. 查看网络服务器 5. 本地环回 6. 云服务器上的网络测试 7. 服务端ip绑定问题 二、实现一个简单的UDP网络程序 1. 单向通信 1.1 服务端.hpp文件 1.2 服务器的.cpp文件 1.3 客户端的.hpp文件 1.4 客户端的.cpp文件 1.4 客户端与服务端的测试 2. 客户端与服务端的双向通信 2.1 使用回调函数 2.2 形成字典 2.3 完成服务端的返回数据和客户端的接收数据 2.4 程序测试 2.4 支持简单的热加载 3. 实现从远端执行linux命令 3.1 popen函数 3.2 服务端处理 3.3 客户端处理 3.4 程序测试 4. 实现一个简单的聊天室 4.1 服务端处理 4.2 客户端处理 4.3 程序测试 三、实现一个简单的windows下的客户端 1.linux下的服务端 1.1 服务端头文件 1.2 服务端源文件 2. windows下的客户端 一、UDP中的socket编程常用接口 1. socket的含义 在未来要实现网络通信大家实际上写的是应用层与传输层交互的代码。而传输层是属于OS的这也就注定了未来我们一定要使用系统调用接口。OS为了支持网络编程也就提供了一系列的网络相关接口。 在上文中讲过了在网络中通过“ip  port”来表示网络中的唯一主机中的某个进程。这里的“ip port”其实被称为“socket”即套接字。 2. sockaddr结构 如果大家仔细观察了socket编程中的接口会发现其中的很多接口都存在一个“struct sockaddr *”结构体指针。那么这里的这个struct sockaddr结构体是什么呢 首要大家要知道套接字其实不止一种。在上面介绍的“ip port”准确来说叫做“网络套接字”。在现在比较主流的套接字有三种分别是网络套接字、原始套接字和unix域间套接字。其它套接字暂时不做考虑。 网络套接字可以用于跨主机通信和本地通信主要使用与应用层开发。 unix域间套接字则只能用于本地通信它的使用接口和网络套接字是非常相似的。可以将其看成和管道相似的东西。 原始套接字的功能比较特殊。上文中说了数据传输时从当层逐次往下传输例如应用层的数据就要通过传输层、网络层、数据链路层层层往下传输。而使用域间套接字就可以做到在应用层跳过传输层直接访问网络层、数据链路层等其他层的内容。这一套接字不是普通用户需要使用一般是一些比较特殊的场景比如数据抓包、网络侦测等才会需要使用。 既然有这多种套接字就说明如果要使用这些套接字就需要设计出对应种类的套接字接口这无疑是非常麻烦的。并且这些套接字的接口中有很多功能都是相同的并没有必要全部重新设计一套。因此套接字的设计者就想设计一套能够适用多种套接字的接口由此就诞生了sockaddr结构。 以网络套接字和unix域间套接字为例。网络套接字和unix套接字的结构体可以看成如下所示 注意这里的in和un应该看做inet和unix。 在网络套接字和unix域间套接字的结构体中前2字节被叫做16位地址类型用于标识是采用网络通信还是本地通信采用AP_INET和AP_UNIX这样的协议家族中的宏来进行区分。在这后面的内容就不一样了。网络套接字中需要填写ip地址和端口号而unix域间套接字可以看成和命名管道是一样的都是通过文件来实现数据交换的要填写的就是该文件的路径名。如果要实现这两种套接字就必须要设计两套不同的接口。 设计者为了减少设计的负担并没有采用设计两套套接字的方案即不使用上面的两个结构体而是使用sockaddr结构体 sockaddr结构体中前2个字节依然代表16位地址类型。后14字节不重要。在使用时依然是定义sockaddr_in或sockaddr_un结构体然后将对应的数据填进去。但是在给函数传参的时候就需要将结构体强转为sockaddr。对应的函数在接收到这个数据时会先提取这个结构体的前2个字节如果它的内容为AF_INET那么就在这个结构体内将其再强转回sockaddr_in。然后再根据强转回的类型采取不同的处理策略。sockaddr_un同理。通过这种方式就能够用同一个函数来接收不同的结构体并进行不同的处理了。这种思想其实就是C中的多态。 大家此时可能会有一个疑惑那就是为什么这里不是void*呢void*可以接收任意类型的参数在这个场景下不是正好适用么这里为什么还要单独设计一个sockaddr结构而不是用void*呢这其实是一个历史遗留问题。void*在这里确实可以用但是网络通信出现的实在是太早了当初设计这批接口的时候void*还没有被加入C的标准中无法使用void*。当void*被加入C标准中时这批接口已经被使用了很久了。而C和C秉持着向下兼容的理念再加上这些接口还是OS中的接口也就不敢对这批接口进行修改只能将其保持原样。 3. socket编程中UDP协议常用接口介绍 这里就介绍几个比较常用的socket接口。 3.1 创建socket文件描述符TCP/UDP、客户端 服务器 socket函数可以用于创建套接字 前文中讲过网络通信其实和进程间通信是一样的只不过网络通信的共享资源是网络资源而已。但是当外部的数据需要写入另一台主机时也是需要通过网络来进行读写的。而socket的作用其实就是创建一个文件并将这个文件与网卡建立联系使其可以接收网络中传输的数据。 domain参数表示你想用哪种方式进行通信 在这些通信方式中最常用的就是最上面的两个。其中AF_UNIX就是unix域间套接字要用的表示本地通信。而AF_INET是网络套接字中使用的表示网络通信。 type表示你的数据是以什么形式传递的 在这里几个宏里面SOCK_STREAM表示字节流而SOCK_DGRAM表示数据报。OS会根据你所选用的方式打开其对应的协议采取对应的方案。 protocol表示使用哪种协议进行通信。这里一般默认填0即可。因为在前面的domain和type填好后其实就已经决定了采用哪种协议。例如domain和type分别填了AF_INET和SOCK_STREAM就表示使用TCP协议进行通信此时该函数就默认使用了TCP协议填0就表示采用前面两个参数所决定的协议。 再来看它的返回值 socket函数在调用成功时会返回一个文件描述符调用失败时返回-1。这里的返回值也印证了前文中说的网络通信中要用文件来读写数据的说法。 此时有人就会奇怪了为什么网络通信可以使用文件来操作呢追根究底就是我们所说的“linux下一切皆文件”。在文件系统创建一个文件时虚拟文件系统会给该文件创建一个文件结构体对象然后创建文件的进程中是有一个文件描述符表的这个文件描述符表是一个数组它的下标就是文件描述符。文件描述符表中保存的是结构体指针指向对应的文件结构体。这些文件结构体中保存了文件的各种属性和一些函数指针。这些函数指针可以指向OS底层中不同的方法既可以是键盘也可以是鼠标。既然这些函数指针可以指向键盘、显示器、鼠标等等硬件当然也可以指向网卡。因此我们就可以用操作文件的方法来操作网络了。当然实际中并不是这么简单还会设计协议栈等内容这里只是为了方便理解所以省去了中间的过程。 3.2 绑定端口号TCP/UDP服务器 当套接字创建好后就有了一个文件可以用于收发数据了。但是这里仅仅是拿到了该文件的文件描述符而这个文件将来要用于那个ip地址下的哪个进程通信这里还没有指明。 此时就需要用bind函数进行绑定 该函数可以用于与一个创建出来的socket绑定。简单来讲就是告诉一个文件它未来要与哪个ip下的哪个进程通信。 1sockfd 就是使用socket创建套接字时返回的文件描述符。 2addr addr就是一个sockaddr *的结构体指针。因为大家在这里一般是用网络通信所以这里就拿sockaddr_in来解释一下这个结构体应该怎么用。 先创建一个sockaddr_in的结构体对象然后转到它的类型定义中去。在定义时可以用bzero接口将结构体清空 s表示要清空的数据的指针n表示要清空的数据的大小 注意在创建sockaddr_in结构体对象时要包含arpa/inet.h头文件该头文件在bind接口的文档中没有写。如果没有这个头文件就无法创建sockaddr_in结构体对象。 转到类型定义后就可以看到如下内容 这就是sockaddr_in结构体中的内容。在这里面主要关注三个内容。 SOCKADDR_COMMON。转到它的类型定义中去 在上图的内容中就可以发现这里其实就是通过这个接口生成了一个sa_family_t的变量。有人可能不理解这里的##是什么意思这其实就是拼接。假设传进来的sa_prefix的内容是A_那么这里就会拼接出A_family。 重新看回上面的结构体。上文中说了在网络通信中要告诉文件它要与哪个ip下的哪个进程相通信。这个结构体中的sin_port就是端口号in_port_t就是表示2字节的整数的类型。 sin_addr就是接收ip地址。转到它的类型定义中 可以看到这个结构体中就包含了一个s_addr它的类型为in_addr_t其实就是uint32_t的重命名表示一个4字节32位的整数用于接收ip地址。 此时大家可能就会有所疑惑了。ip地址这个东西在实际中无论是在windows下还是在linux下查找时返回的都是一个“点分十进制”的字符串那为什么传的时候却是传一个4字节的整数呢这里大家要搞清一个问题“点分十进制”的ip地址它仅仅是一个表现形式有较好的可读性是为了方便外层的用户看的。但在实际的网络传输中ip地址是一个4字节32位的整数。 原因就是点分十进制的字符串形式的ip地址除了可读性好以外一无是处。在使用时不仅需要将其通过字符串分割的方式来进行解析并做各种处理而且占用还高。一个4个位上全是3位数的ip就需要占15字节。反之整数形式的ip不仅方便读取而且占用还低。因此在网络通信中使用的都是整数风格的ip 1. 点分十进制的ip地址转为4字节整数 但是在实际上因为点分十进制的ip地址可读性好大众接受程度高所以在实际使用中也一般是传字符串形式的ip。此时就需要将字符串转化为4字节的整数。要转化也很简单以“.”作为分隔符获取到对应的数字然后通过计算将字符串数字转化为整型数字即可。这个动作并不需要用户自己写直接调用系统接口inet_addr即可 这个函数中会做两件事。第一件事就是帮我们把点分十进制的ip地址转化为4字节整数第二件事就是调用htonl函数。因为这个ip地址是需要传给另一台主机的所以会存在大小端的问题。该函数中会自动调用htonl函数把一个4字节32位的长整数转化为网络字节序。 2. 字节整数转化点分十进制的ip地址 那4字节的整数ip是如何转化为字符串的呢转化方式很简单通过一个结构体就可以实现。假设现在有数字12345我们只需要定义一个结构体如下 这个结构体中有4个unsigned char类型的变量。有了这个结构体后只需要将对应的数字强转为struct ip即可。此时这个数字中的字节就会被默认以char的类型逐个读取。读取完成后再将读取到的内容转化为字符。最后将转化出的字符加上“.”拼接起来即可。 当然在实际中这个过程并不需要我们来做直接使用系统中提供的inet_ntoa接口即可 该接口就是用于将一个in_addr的结构体中的整数ip转化为点分十进制的ip地址。至于这里为什么是结构体是因为在网络中接收ip和端口号等内容都是通过结构体来实现的。网络套接字所采用的就是sockaddr_in结构体 这个结构体中还有一个结构体sin_addr它里面存储的就是4字节整数的ip地址 至于sockaddr_in结构体中的最后一个成员就是用于填充数据的不用管。其实这个结构体中的内容可以看成如下图所示 3addrlen 这个参数就是传入的结构体的大小。 最后再来看bind的返回值 在绑定成功时返回0失败则返回-1。 3.3 接收数据 如果想接收数据可以使用recvfrom接口  第一个参数sockfd表示用socket接口创建的用于通信的文件的文件描述符。 第二个参数buf是一个缓冲区写入的数据都需要进入这个缓冲区内。 第三个参数len表示缓冲区的大小。 第四个参数fiags表示如何获取数据。一般填成0即可表示阻塞式获取数据。当有数据传入就获取没有就阻塞等待。 第五个和第六个参数src_addr和addrlen都是输出型参数。src_addr是一个结构体用于接收客户端的ip和端口号等内容。addrlen则是这个用于接收这个结构体的大小。 再来看它的返回值 成功返回接收到的数据的大小。失败则返回-1。 3.4 发送数据给远端服务器 要发送数据给远端服务器就可以使用sendto接口 这个接口的参数和接收数据的recvfrom接口一模一样含义也差不多。 第一个参数sockfd就是socket接口的返回的文件描述符。 第二个参数buf就是要传入的数据。 第三个参数len就是传入的数据的大小。 第四个参数flags表示如何发送。默认填0为阻塞式发送。 第五个参数dest_addr它是一个结构体里面包含了要向谁发送数据的信息。 第六个参数addrlen表示这个结构体的大小 再来看它的返回值 发送成功时返回发送的数据的字节数发送失败时返回-1 4. 查看网络服务器 如果想查看你的liunx中的网络服务器状况可以输入“netstat -nuap命令 该命令在普通用户中使用时会隐藏部分内容如果想看到所有内容可以加上sudo。 在该命令的选项中n表示将所有内容尽可能的以数字形式显示a表示显示所有的服务器p表示显示对应的进程。 5. 本地环回 在我们的linux中一般都存在一个“127.0.0.1”的ip地址这个ip地址是“一个本地环回”。当使用该ip进行网络传输时传输的数据会从应用层向下传输当到达数据链路层时就会向上返回不会到达物理层 该ip一般可以用于服务器代码的测试。 6. 云服务器上的网络测试 当大家写好程序后可能会去使用自己的云服务器的公网ip进行测试。但是云服务器是虚拟化的服务器不能直接bind它的公网ip。当然如果你是在自己的linux机器例如虚拟机或独立真实的linux环境下就可以bind自己的公网ip。 7. 服务端ip绑定问题 在服务端中最好不要指明绑定某一个ip。因为可能出现大家会通过多个ip地址向同一个端口号发数据的情况如果此时该端口号绑定了一个特定的ip就会导致除了指明的那个ip以外其他ip传过来的数据都不会被接收。因此在实际中要绑定的结构体中的ip最好使用INADDR_ANY 这个宏其实就是一个全0的ip可以接收任意ip。 二、实现一个简单的UDP网络程序 因为TCP协议的网络程序实现起来比较复杂所以这里用UDP协议来实现一个网络程序。 1. 单向通信 在这里先写一个简单的可以进行单向通信即客户端向服务端发送消息的程序。 1.1 服务端.hpp文件 1整体结构 在服务端中我们用一个类来封装服务端需要进行的操作。在这个服务端中该服务端需要有一个提供通信的ip地址和一个端口号来让客户端传入数据所以需要有对应的ip和port。同时上文中说了要进行网络通信首先就需要有通过socket来创建套接字形成一个文件与网卡建立联系所以就还需要一个能保存文件描述符的变量来保存socket返回的文件描述符 #pragma once #include iostream #include string #include functional #include stdlib.h #include string.h #include strings.h #include sys/types.h #include sys/socket.h #include errno.h #include arpa/inet.h #include netinet/in.h #include unistd.hnamespace server {static const std::string defaultip 0.0.0.0;//提供一个默认ipstatic const int gnum 1024;enum//设置退出码{USAGE_ERR 1,SOCKET_ERR,BIND_ERR};class udpServer{public:udpServer(const uint16_t port, const std::string ip defaultip)//构造函数:_port(port), _ip(ip), _sockfd(-1){}~udpServer(){}private:uint16_t _port;//创建端口号uint16_t代表一个2字节的整数类型std::string _ip;//ip地址int _sockfd;//接收socket函数返回的文件描述符}; } 在这里的defaultip是一个全0的ip用于初始化ip。同时为了方便这里也通过联合体定义了 几种错误。 2服务器初始化 服务器的整体结构有了后在启动服务器之后就需要对服务器进行初始化。初始化的步骤就比较简单了首先是调用socket函数创建套接字然后调用bind函数进行绑定。这两个函数的使用方法在上文中已经讲解过了就不过多赘述 void initServer() // 服务器初始化 {//1. 使用socket接口创建套接字_sockfd socket(AF_INET, SOCK_DGRAM, 0);//采用网络通信数据报形式。即UDP协议if(_sockfd -1){std::cerr socket error: errno strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout socket success: errno : _sockfd std::endl;//2. 使用bind接口进行绑定。在服务端中一定要自己bind。因为服务器需要有一个明确的port以供使用不能随意更改struct sockaddr_in local;//创建结构体对象bzero(local, sizeof(local));//将结构体中的数据清0local.sin_family AF_INET;//协议家族采用本地通信还是网络通信local.sin_port htons(_port);//通过htons将端口号转为为网络字节序// local.sin_addr.s_addr inet_addr(_ip.c_str());//将点分十进制的ip通过inet_addr转化为4字节整数并转化为网络字节序local.sin_addr.s_addr htonl(INADDR_ANY);//支持传入任意ip方便不同ip传入数据给同一端口号int n bind(_sockfd, (struct sockaddr *)local, sizeof(local));//调用bind进行绑定if(n -1)//检测是否绑定成功{std::cerr bind error: errno strerror(errno) std::endl;exit(BIND_ERR);}//服务器初始化完成 } 注意在初始化时不要让服务器显式bind一个ip地址。因为将来可能会有多个不同ip的客户端通过某个端口向该服务器发送数据如果显式绑定某个ip就会导致只有这个ip通过某个端口传来的数据才会被服务器接收其他ip通过同一个端口传来的数据都无法被接收。 所以填写ip的时候最好就是填写为“INADDR_ANY”表示可以接收任何ip通过bind的端口传来的数据。 3运行服务器 当服务器初始化完成后就可以准备运行服务器了。大家知道一个服务器运行起来后除非用户自行关闭否则在正常情况下它都是不会退出的。因此服务器在运行状态就是一个死循环。在这里的这个服务器中当前主要是用它来接收客户端发来的数据所以直接使用recvfrom函数来接收即可。同时这里为了方便测试所以实现让客户端发送数据然后服务端打印数据即可 void Start() // 服务器启动 {char buffer[gnum];//缓冲区while(true)//一般来讲一个服务器在启动后其实是死循环的。{ //在正常情况下只要用户不退出服务器就不会退出struct sockaddr_in peer; // 创建结构体对象bzero(peer, sizeof(peer));socklen_t len sizeof(peer);ssize_t s recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)peer, len);//接收客户端传过来的数据if(s 0){buffer[s] 0;std::string clientip inet_ntoa(peer.sin_addr);//获取ip,并将4字节整数的网络系列转化为点分十进制uint16_t clientport ntohs(peer.sin_port);//获取端口号std::string message buffer;std::cout clientip [ clientport ]# message std::endl;}} } 1.2 服务器的.cpp文件 要将这个服务器启动在这里让它以“程序名 port”的形式启动。在这里就需要用到main函数中的参数了。大家知道main函数中是有参数的第一个参数argc表示传进来的命令个数第二个参数argv[]中则保存了这些命令。第三个参数则是环境变量。 在这里因为是以“程序名 port”的格式启动所以要传两个参数如果不足两个参数就结束。按照格式启动后就可以创建一个服务器的对象然后初始化并启动了 #includeudpServer.hpp #includememoryusing namespace server;//127.0.0.1 本地换回。在本主机内的应用层流到数据链路层然后向上传输。不会达到物理层。 //本地换回主要用于服务器代码的测试 void Usage(std::string proc) {std::cout \nUsage:\n\t proc local_port\n\n; }//运行程序时按照./udpServer port的形式运行 int main(int argc, char *argv[])//显示调用main函数中的参数 {if(argc ! 2)//参数不足两个表示未按照规定传入ip和port{Usage(argv[0]);//打印错误exit(USAGE_ERR);}uint16_t port atoi(argv[1]);//注意从argv中拿到的参数是字符串要将其转为整数std::unique_ptrudpServer usrv(new udpServer(port));//使用智能指针来构建对象使其能够自动析构usrv-initServer();//初始化服务器usrv-Start();//启动服务器return 0; } 1.3 客户端的.hpp文件 1整体结构 客户端主要用于向服务器发送数据所以它和服务器一样都需要有自己文件描述符。然后在客户端中该客户端需要通过某个ip下的某个port向服务器发送数据所以也需要有对应的ip和port来保存这些数据用于后面的运行 #pragma once #include iostream #include string #include stdlib.h #include string.h #include strings.h #include sys/types.h #include sys/socket.h #include errno.h #include arpa/inet.h #include netinet/in.h #include unistd.hnamespace client {enum{USAGE_ERR 1, SOCKET_ERR};class udpClient{public:udpClient(const std::string serverip, const uint16_t _serverport)//构造函数:_serverip(serverip), _serverport(_serverport), _sockfd(-1), _quit(0){}~udpClient(){}private:int _sockfd;//文件描述符std::string _serverip;//向该ip地址发送数据uint16_t _serverport;//想该端口号发送数据bool _quit;}; } 在这里的_quit仅仅只是用于标识客户端的运行状态的。 2客户端的初始化 客户端的初始化就比服务端的初始化简单了。客户端初始化用户只需要创建套接字即可。无需bind。注意这里的无需bind是指用户无需自己写bind而是让OS自动生成自动bind。因为在未来网络中会有无数客户端向服务器发送数据如果用户自己bind了端口就可能出现两个不同的客户端拥有相同的port并且向同一个ip下的同一个port发送数据。此时就会导致后发数据的客户端因为该port已经被占用而无法向服务器发送数据只能阻塞等待。 基于以上原因在客户端中用户无需自己bind也非常不建议用户自己bind。 void initClient()//初始化客户端{//1. 创建套接字_sockfd socket(AF_INET, SOCK_DGRAM, 0);if(_sockfd -1){std::cerr socket error: errno strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout socket success: errno : _sockfd std::endl;//2. 客户端也需要使用bind绑定端口因为在未来网络通信时无论是客户端还是服务端都需要通过ipport来通信//但是在客户端这里不需要显式bind。由OS自动bind即可。因为客户端中只需要有一个端口标定唯一性即可。//更重要的是写服务器的是一家公司而使用服务器的却有无数家公司。如果客户端显式绑定一个port就可能出现公司A的//某个进程已经绑定了某个端口号而另一个公司的某个进程也绑定同一个端口号的情况。如果这两个进程刚好向同一个服务器端口//发送数据就会出现A公司的程序先启动占用了该端口号B公司的程序后启动就无法使用该端口号进而出现无法网络通信的情况} 3客户端运行 在这里的客户端只需要承载向服务器发送数据的工作所以只需要调用sendto函数发送数据即可。该函数的使用在上文中也已经讲过了这里就不再赘述 void run()//程序启动 {std::string message;while(!_quit){std::cout Please Enter#;getline(std::cin, message);struct sockaddr_in server;bzero(server, sizeof(server));server.sin_family AF_INET;//网络通信server.sin_port htons(_serverport);//端口号server.sin_addr.s_addr inet_addr(_serverip.c_str());//将获取到的点分十进制的ip地址转化为4字节的整数sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)server, sizeof(server));//在sendto时OS检测到没有绑定端口自动生成一个} } 1.4 客户端的.cpp文件 在客户端中因为是要向某个服务端发送数据所以按照“程序名 ip port”的格式建立通信。main中的参数的作用在上文的服务器的.cpp文件中已经讲了就不过多赘述了 #includeudpClient.hpp #include memory #include assert.h #include netdb.husing namespace client;void Usage(std::string proc) {std::cout \nUsage:\n\t proc local_ip local_port\n\n; }//./udpClient ip port int main(int argc, char *argv[]) {if(argc ! 3)//启动程序方式不符{Usage(argv[0]);//打印错误exit(USAGE_ERR);}std::string serverip argv[1];//向哪个服务端发送数据uint16_t serverport atoi(argv[2]);//向服务端的哪个端口号发送数据std::unique_ptrudpClient ucli(new udpClient(serverip, serverport));ucli-initClient();ucli-run();return 0; } 1.4 客户端与服务端的测试 因为大家可能没有两台不同的linux机器所以在测试时无法在不同ip地址下测试。但是在大家的机器中有一个统一的地址“127.0.0.1”。该地址是一个本地环回代表的就是你自己的机器通过这个ip发送的数据会在协议层中向下传输当到了数据链路层后就会向上返回可以用于测试服务器代码。 在linux下打开两个会话窗口并分别运行客户端和服务端 在上图中左侧为服务端右侧为客户端。可以看到客户端发送给服务端的数据服务端确实可以收到此时就已经完成了一次简单的udp协议的网络通信。 2. 客户端与服务端的双向通信 大家知道在实际中客户端将数据发送给服务端后并不是让服务端打印一遍数据就完事了而是要让服务器将这份数据拿取执行特定的操作然后向客户端返回一份数据。所以在这里就基于上面的单向通信的代码实现一个可以让服务端向客户端返回数据的程序。 在这里就实现一个简单的翻译程序该程序可以将英文翻译为中文。客户端将一个英文字符串发送给服务端服务端则将翻译结果返回给客户端。 2.1 使用回调函数 为了让服务端能够拿到客户端传过来的数据进行翻译所以首先就要在创建对象时将对应的方法传过去所以在服务端的类中新加入一个变量用来执行指定的任务 在这个函数中该函数也要知道是哪个客户端向其发送了数据然后还要使用这些数据。所以使用function包装器形成一个类名返回值为void参数为分别是ip、端口号和字符串数据。 在启动服务器的时候就要调用该回调函数 在服务端的.cpp文件中创建对象时就要传入对应的执行函数 2.2 形成字典 要实现这一功能首先就要有一份字典。这里为了方便所以就自己创建一个dict.txt文件该文件中写入几个对应的中英文翻译 有了这写字符串后就要将这些字符串保存到一个unordered_map中以这种方式形成映射关系。 在这里使用的是C中的文件操作接口当然如果你想用C中的操作接口也是一样的。为了测试这些是否能正常读取文件中的内容就先将文件中的内容打印出来 在main函数中加入该函数然后运行程序 可以正常读取。  文件正常读取测试通过后就可以修改dictInit函数的代码将文件中的数据映射到unordered_map中了。 因为在文件中的数据是以“”进行区分的所以在进行映射前首先要切割字符串按照特定的标志位将文件中的字符串切割为中英两个然后再插入 完成字符串插入后再写一个测试函数来测试这些字符串是否被正常切割并插入其中 将字典初始化函数和测试函数都添加到main中运行程序 根据打印的结果可以知道这份数据的插入是正常的。 此时就可以着手让服务端将翻译结果返回给客户端了。 2.3 完成服务端的返回数据和客户端的接收数据 字典准备好后就可以开始让客户端直接进行数据的收发了。首先是让客户端返回经过字典查询后的数据这一操作就可以在之前提前准备好的回调函数中完成 服务端向客户端发送了数据客户端就需要对这份数据进行接收。这个工作在客户端的运行函数中调用recvfrom函数即可 2.4 程序测试 将程序修改好后就可以着手测试了。将字典的初始化函数添加进主函数然后运行即可 可以看到在客户端这一方就成功的接收到了服务端经过查询后得到的结果。运行正常。 2.4 支持简单的热加载 在上面的程序中如果想更新字典中的内容就必须要关闭服务端让服务端重新加载字典才行。但如果我们不想关闭服务端就更新字典呢方法很简单使用信号就可以了。大家知道信号是可以被捕捉的可以通过捕捉信号然后给它传函数的方式让程序执行其他功能。在这里就可以利用信号的这一特性捕捉一个特定的信号然后给这个信号的传一个执行函数再该函数中再次加载字典即可。 写起来也很简单在main函数中使用signal函数然后在执行函数中调用字典初始化函数即可 写好后如果你想在服务端运行的时候更新字典的内容直接发送2号信号给服务端即可。这里就不再演示了。 3. 实现从远端执行linux命令 既然客户端中可以发送字符串数据给服务端进行处理那我们是不是也可以通过客户端向服务端发送一个linux命令让服务端执行该命令并将结果返回给客户端呢答案当然是可以的。 同样是以上面的程序为基础进行修改。要让服务端执行linux命令只需要在服务端中重新传一个回调函数即可。如果大家看过我以前的文章在以前的文章中我们就已经完成过一个简易的shell利用fork创建子进程、exec进程替换等函数实现了使用自己的程序来调用linux命令的功能。在这里其实是可以直接将那份代码拷贝过来放到回调函数中然后将一些地方修改一下就可以实现从远端执行linux命令的功能了。 3.1 popen函数 在这里就不用去拷贝代码过来的方法而是popen函数实现 这个函数的第一个参数command是一个字符串第二个type则是打开文件的方式即C中的r、w、a等。其返回值为打开的管道文件的文件指针。 这个函数的作用大家可以看成它就相当于“pipe” “fork” “exec” popen。在这个函数中它会解析你传入的字符串然后创建子进程和使用程序替换来运行该字符串中的命令再将结果写入到管道文件中。如果你想从这个管道文件中读取数据就传入“r”如果是想写入数据将传入“w”是以读端打开还是以写端打开取决于用户自己。 3.2 服务端处理 有了popen函数要实现远端执行linux命令就很简单了。服务端以读的方式调用popen函数然后将执行命令后获取到的内容从管道中取出再将这些内容返回给客户端即可。 同样的首先在服务端要接收到数据然后让服务端拿着这份数据去执行对应的命令 再将命令的结果拿到一个字符串内然后再将这个字符串发送给客户端即可 3.3 客户端处理 在客户端中就没什么需要改的了当然 你也可以修改一下运行客户端函数中的打印来与上面字典翻译的处理相区分。 3.4 程序测试 程序修改好后将服务端中传给服务端对象的回调函数修改为我们写的远端执行linux命令的函数然后运行程序 可以看到在服务端就确实获取到了对应linux命令执行后的结果。但是这个程序非常简易还有很多命令是无法支持的比如需要一些特殊处理的top命令等。但是一些比较基础的ls、touch等命令是可以支持的。 4. 实现一个简单的聊天室 现在我们已经实现了客户端到服务端的单向通信和客户端与服务端的双向通信此时就可以基于以上的程序实现一个多客户端通过一个服务端进行交互的程序了。在这里就实现一个简单的聊天室让服务端将接收到的消息同时转发给多个客户端以实现用户在不同的客户端下进行聊天的功能。 4.1 服务端处理 在这个聊天室的程序中用一个user类来保存用户的相关信息再用一个onlineuser类来统一保存所有在线的用户。将这两个类都写在另一个文件中以方便区分。在onlineuser类中要提供登录、下线、判断用户是否在线和向所有在线用户发送信息的功能 #pragma once #include iostream #include unordered_map #include string #include stdlib.h #include string.h #include strings.h #include sys/types.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include unistd.hstruct user//记录用户信息 { public:user(const std::string ip, const uint16_t port):_ip(ip), _port(port){}~user(){}std::string ip() {return _ip;}uint16_t port() {return _port;}private:std::string _ip;uint16_t _port; };class onlineUser//记录在线用户,以ip port来标识 { public:onlineUser(){}void addUser(const std::string ip, const uint16_t port)//添加在线用户{std::string id ip - std::to_string(port);users.insert(std::make_pair(id, user(ip, port)));//将标识用户的id与用户添加到unordered_map中}void delUser(const std::string ip, const uint16_t port)//删除在线用户{std::string id ip - std::to_string(port);users.erase(id);}bool isOnline(const std::string ip, const uint16_t port)//判断用户是否在线{std::string id ip - std::to_string(port);return users.find(id) users.end() ? false : true;}void broadcastMessage(int sockfd, const std::string ip, const uint16_t port, const std::string message)//通过sockfd将信息发送给所有用户{for(auto user : users){struct sockaddr_in client;bzero(client, sizeof(client));client.sin_family AF_INET;client.sin_port htons(user.second.port());client.sin_addr.s_addr inet_addr(user.second.ip().c_str());std:: string s ip - std::to_string(port) # ;//带上id打印的时候方便看是否是不同客户端s message;sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)client, sizeof(client));}}~onlineUser(){} private:std::unordered_mapstd::string, user users;//将用户存储在unordered_map中 }; 在登录功能中就将对应的用户信息保存到unordered_map中下线则是将用户数据删除。要实现向所有用户发送信息的功能也是比较简单的直接遍历unordered_map中的数据向其中所有的用户发送数据即可。 有了上面的功能就只需要让服务端将从客户端的数据拿到并根据自行判断用户是否在线决定是否转发消息了。 onlineUser onlineusers;//创建对象保存发送信息过来的客户端ip和port void routeMessage(int sockfd, std::string clientip, uint16_t clientport, std::string message) {if(message online) onlineusers.addUser(clientip, clientport);//用户输入online表示上线将其加入对象中if(message offline) onlineusers.delUser(clientip, clientport);//用户输入offline表示下线从对象中删除if(onlineusers.isOnline(clientip, clientport))//如果用户在线{onlineusers.broadcastMessage(sockfd, clientip, clientport, message);//通过socket创建的文件将信息发送给所有在线用户}else//用户不在线,返回信息要求用户在线{struct sockaddr_in client;bzero(client, sizeof(client));client.sin_family AF_INET;client.sin_port htons(clientport);client.sin_addr.s_addr inet_addr(clientip.c_str());std::string response 未登陆,请输入“online”以上线;sendto(sockfd, response.c_str(), response.size(), 0, (sockaddr *)client, sizeof(client));} } 正常来讲应该是要有登录之类的功能的这里为了简便就没有提供登录等功能而是直接让用户输入“online”来告诉服务端该用户已经上线然后输入“offline”来告诉服务端该用户下线。 4.2 客户端处理 在客户端这里客户端同样只有两个事情要做向服务端发送数据和接收服务端返回的数据。在这里为了更好的使用可以创建一个从线程让该线程负责从服务端读取数据。然后让主线程负责向服务端传输数据 static void *readMessage(void *args)//执行读取数据的操作{int sockfd *(static_castint *(args));pthread_detach(pthread_self());//线程分离while (true){char buffer[1024];struct sockaddr_in temp;socklen_t temp_len sizeof(temp);ssize_t n recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)temp, temp_len);if (n 0)buffer[n] 0; // 将最后一位置0表示到这里结束// std::cout 服务器翻译结果: buffer std::endl;//字典翻译std::cout buffer std::endl; // 远端执行linux命令}return nullptr;}void run()//程序启动{pthread_create(reader, nullptr, readMessage, (void *)_sockfd);struct sockaddr_in server;bzero(server, sizeof(server));server.sin_family AF_INET; // 网络通信server.sin_port htons(_serverport); // 端口号server.sin_addr.s_addr inet_addr(_serverip.c_str()); // 将获取到的点分十进制的ip地址转化为4字节的整数std::string message;while(!_quit){//1. 向服务端发送数据// std::cout Please Enter#;fprintf(stderr, Please Enter# );fflush(stderr);getline(std::cin, message);sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)server, sizeof(server));//在sendto时OS检测到没有绑定端口自动生成一个//2. 接收服务端返回的数据。由reader线程执行}} 客户端的类的成员变量 至于其他地方客户端中就没有什么需要修改的地方了。 4.3 程序测试 这个简易的聊天室程序其实就是在双向通信的代码的基础上新添加了上面的模块使得程序可以实现从服务端向单个客户端转发数据到服务端向所有在线客户端转发数据的功能。因此这个聊天室程序的总体框架上和双向通信代码是没有区别的只是新增了点模块。 有了这个聊天室程序后就可以着手测试了。因为这个程序我们并没有一个前端可以以供区分自己输入的数据和聊天框中的数据就会导致这些数据杂糅在一起不方便看。因此这里就通过管道的方式将服务端返回给客户端的数据都通过放入到一个管道文件中然后再通过另一个窗口从这个管道文件中读取数据。 linux中创建命名管道的命令是“mkfifo 管道文件名”。在这里就创建了fifo和fifo1两个管道文件以模拟出两个不同的客户端 打开5个会话窗口在其中一个窗口运行服务端 再在另外四个窗口分别执行“./程序名 127.0.0.1 8080 管道文件名”和“cat 管道文件名” 在上图中执行了“./程序名 127.0.0.1 8080 管道文件名”命令的窗口充当输入界面而执行了“cat 管道文件名”命令的窗口则充当聊天界面。在执行了“./程序名 127.0.0.1 8080 管道文件名”命令的窗口输入test 可以看到下面的两个窗口都反馈出了未登陆的信息。当然这里双方不能通信是因为还没有登陆所以服务端只是单独向发送数据的客户端返回信息。 输入“online”登陆 此时就提示客户端某个ip和端口下的用户登陆了。这里左边打印两条登陆右边打印一条登陆是因为左边先登陆先打印出登陆语句。然后右边的后登陆登陆成功后服务端将该用户的信息转发给了所有在线用户。因此左边的客户端收到两条信息。它们的端口号不同也证明了这一点。 此时就可以开始聊天了 可以看到在上图中两个不同的客户端就在同一个服务端下聊天了完成了通信。可以通过它们的端口号来进行区分。 如果你只有一台linux机器就可以使用127.0.0.1这个本地换回地址来测试你的网络代码。如果你有不同的linux机器就可以通过不同的ip地址来进行测试。结果都是一样的。 三、实现一个简单的windows下的客户端 大家可以将自己的linux机器充当服务器然后在windows下完成一个简单的客户端再通过windows客户端来实现网络通信。如果大家愿意甚至可以对windows下的客户端添加图形化界面例如使用easyx等软件来实现以此完成一个简单的以linux充当消息转发服务器的windows下的简易聊天室。 windows下的网络通信并不是本文的重点所以就简单的实现一个windows下的客户端向linux服务端发送消息然后linux服务端再将消息处理一下后转发回去的程序。 1.linux下的服务端 1.1 服务端头文件 服务端的头文件和上面的程序的头文件是一模一样的这里就不再多说。 #pragma once #include iostream #include string #include functional #include stdlib.h #include string.h #include strings.h #include sys/types.h #include sys/socket.h #include errno.h #include arpa/inet.h #include netinet/in.h #include unistd.hnamespace server {static const std::string defaultip 0.0.0.0;//提供一个默认ipstatic const int gnum 1024;enum//设置退出码{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,OPEN_ERR};class udpServer{typedef std::functionvoid(int, std::string, uint16_t, std::string) func_t;//包装器重名一个类型public:udpServer(const func_t callback, const uint16_t port, const std::string ip defaultip)//构造函数:_callback(callback), _port(port), _ip(ip), _sockfd(-1){}void initServer() // 服务器初始化{//1. 使用socket接口创建套接字_sockfd socket(AF_INET, SOCK_DGRAM, 0);//采用网络通信数据报形式。即UDP协议if(_sockfd -1){std::cerr socket error: errno strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout socket success: errno : _sockfd std::endl;//2. 使用bind接口进行绑定。在服务端中一定要自己bind。因为服务器需要有一个明确的port以供使用不能随意更改struct sockaddr_in local;//创建结构体对象bzero(local, sizeof(local));//将结构体中的数据清0local.sin_family AF_INET;//协议家族采用本地通信还是网络通信local.sin_port htons(_port);//通过htons将端口号转为为网络字节序local.sin_addr.s_addr htonl(INADDR_ANY);//支持传入任意ip方便不同ip传入数据给同一端口号int n bind(_sockfd, (struct sockaddr *)local, sizeof(local));//调用bind进行绑定if(n -1)//检测是否绑定成功{std::cerr bind error: errno strerror(errno) std::endl;exit(BIND_ERR);}}void Start() // 服务器启动{char buffer[gnum];//缓冲区while(true)//一般来讲一个服务器在启动后其实是死循环的。{ //在正常情况下只要用户不退出服务器就不会退出struct sockaddr_in peer; // 创建结构体对象bzero(peer, sizeof(peer));socklen_t len sizeof(peer);ssize_t s recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)peer, len);//接收客户端传过来的数据if(s 0){buffer[s] 0;std::string clientip inet_ntoa(peer.sin_addr);//获取客户端ip,并将4字节整数的网络系列转化为点分十进制uint16_t clientport ntohs(peer.sin_port);//获取客户端端口号std::string message buffer;std::cout clientip [ clientport ]# message std::endl;_callback(_sockfd, clientip, clientport, message);//执行回调函数}}}~udpServer(){}private:uint16_t _port;//创建端口号uint16_t代表一个2字节的整数类型std::string _ip;//ip地址int _sockfd;//接收socket函数返回的文件描述符func_t _callback;//回调函数}; } 1.2 服务端源文件 这里只实现服务端接收消息并转回给客户端的功能所以只是在上面的程序的基础上修改下回调函数也不过多说。 #include udpServer.hpp #include memory #include unordered_map #include fstream #include signal.h #include stdio.h #include string.husing namespace server; void routeMessage(int sockfd, std::string clientip, uint16_t clientport, std::string message) {std::string response_message message;response_message [server echo];//返回数据struct sockaddr_in client;bzero(client, sizeof(client));client.sin_family AF_INET;client.sin_port htons(clientport);client.sin_addr.s_addr inet_addr(clientip.c_str());sendto(sockfd, response_message.c_str(), response_message.size(), 0, (sockaddr *)client, sizeof(client)); }void Usage(std::string proc) {std::cout \nUsage:\n\t proc local_port\n\n; }//运行程序时按照./udpServer port的形式运行 int main(int argc, char *argv[]) {if(argc ! 2)//参数不足两个表示未按照规定传入ip和port{Usage(argv[0]);//打印错误exit(USAGE_ERR);}uint16_t port atoi(argv[1]);//注意从argv中拿到的参数是字符串要将其转为整数std::unique_ptrudpServer usrv(new udpServer(routeMessage, port));usrv-initServer();//初始化服务器usrv-Start();//启动服务器return 0; } 2. windows下的客户端 其实windows下的网络编程和linux下的网络编程使用的接口是基本一致的只有几个地方有所区别。 首先windows中的网络通信接口都是包含在WinSock2.h头文件下的。 其次在vwindows中要使用网络都是通过网络库来使用的。所以要定义一个“#pragma comment(lib, ws2_32.lib)”的宏表示该程序中使用了ws2_32.lib这个库。这个“ws2_32.lib”中的w代表windowss代表socket2代表版本32代表32位机器。 然后要启动WinSock并初始化网络库和选择你需要使用的库的版本。 在上面的代码中WSAData其实就是一个结构体通过WSAStartup函数将该程序使用的库的版本保存在wsd中其中MAKEWORD(2, 2)就是表示要使用2.2版本的库。在编译时编译器就会拿着这个版本去对应的库中寻找和使用对应版本的库。 最后一点就是要释放在这个程序中使用的库的资源直接调用WSACleanup函数即可。 在windows中的客户端与linux下的客户端就只有这四个地方不同其他函数的使用都是一模一样的依然是创建套接字发送数据和接收数据。 还有一点小区别就是linux中socket函数是用int来接收的而windows中则是用SOCKET来接收的其实它就是对unsigned __int64类型即64位的无符号整数类型进行了封装。和int没有太大区别。 #pragma once #define _WINSOCK_DEPRECATED_NO_WARNINGS //上面的宏是用于防止inet_addr函数报错 #include iostream #include string #include string.h #include WinSock2.h//windows下使用网络所需的头文件#pragma comment(lib, ws2_32.lib)//表示使用网络库using namespace std;const uint16_t serverport 8080; string serverip 43.142.247.17;int main(int argc, char argv[]) {WSAData wsd;if (WSAStartup(MAKEWORD(2, 2), wsd) ! 0)//初始化WinSock并选择要使用哪个版本的库{cout WSAStartup Error WSAGetLastError() endl;return 0;}elsecout WSAStartup Success endl;//1. 创建套接字SOCKET sock socket(AF_INET, SOCK_DGRAM, 0);if (sock SOCKET_ERROR){cout socket Error WSAGetLastError() endl;return 1;}elsecout scoket Success endl;//2. 绑定套接字。在客户端中也需要bind但无需用户自己bind//3. 建立连接进行通信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());//要向哪个ip发送数据#define NUM 1024string line;char buffer[1024];while (true){//3.1 发送数据cout Please Enter# ;getline(cin, line);int n sendto(AF_INET, line.c_str(), line.size(), 0, (struct sockaddr*)server, sizeof(server));if (n 0){cout sendto error! endl;break;}//3.2 接收数据struct sockaddr_in peer;int peerlen sizeof(peer);memset(peer, 0, peerlen);buffer[0] 0;//清空n recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)peer, peerlen);if (n 0){buffer[n] 0;//设置\0标志结尾cout server返回的消息: buffer endl;}elsebreak;}closesocket(sock);//关闭套接字可用可不用WSACleanup();//释放使用的库的资源return 0; } 此时就可以进行通信了。通信方式和linux下的一样直接启动程序即可。 如果大家遇到无法通信的情况可能是你所使用的云服务器的公网ip或端口号未开放数据被拦截了。因为大家所使用的云服务器大多数都是默认设置为禁止使用公网ip对外通信的所需需要大家去自己买的云服务器的官网下登录自己的账户去打开自己的云服务器ip和端口这里就不再赘述了。
http://www.hkea.cn/news/14274954/

相关文章:

  • 学网站开发培训机构做医疗网站颜色选择
  • 用哪个做网站demo网站服务器租用有什么好
  • 旅游景点网站设计动漫网页设计报告
  • 建站之星备案广告策划案例范文
  • 门户网站建设的步骤网页设计作业下载
  • 网站权限怎么设置方法国内十大网站建设公司
  • 广西网联电线电缆有限公司网站优化的学习
  • 卡盟网站顶图怎么做全国的p2p网站建设
  • 淘宝指数网站论坛推广网站
  • 模板网站建设流程图大连做网站首选领超科技
  • 贵州省城乡建设厅网站太原网页设计培训班
  • Dw做html网站企业网站建设可行性分析表
  • 自做购物网站多少钱wordpress欢迎主题
  • 网站正在建设中界面设计php网站内容管理系统
  • 利用小米路由器mini做网站怎么宣传网站
  • 国外网站 国内访问速度品质好物推荐怎么上
  • asp做的网站缺点公司部门解散调岗不同意有赔偿吗
  • 竹子建站怎么样营销网站建设维护
  • 东莞个人网站制作免费的微信小程序模板
  • 动态素材网站旅行社网站模板
  • 网站开发与管理所对应的职位及岗位西安好的互联网设计公司
  • 海沧网站建设廊坊企业建站模板
  • wordpress 微网站模板Light模板WordPress
  • 邢台移动网站建设网站html地图怎么做的
  • 何炅做的代言网站wordpress案例站点
  • 怎么做彩票网站网页软件开发
  • 网站建设合同2018连衣裙一起做网站
  • 网站建设学习网公司有哪些绿色食品网站源码
  • 搭建个人博客wordpress成都企业网站seo
  • 电子商务网站建设人才官方百度平台