网络和Linux网络_2(套接字编程)socket+UDP网络通信代码

目录

1. 预备知识

1.1 源IP地址和目的IP地址

1.2 端口号port和套接字socket

1.3 网络通信的本质

1.4 TCP和UDP协议

1.5 网络字节序

2. socket套接字

2.1 socket创建套接字

2.2 bind绑定

2.3 sockaddr结构体

3. UDP网络编程

3.1 server的初始化服务器

3.2 server的数据处理Start

3.3 客户端udp_client.cc

3.4 多线程收发数据

本篇完。


1. 预备知识

1.1 源IP地址和目的IP地址

通过上一篇我们知道,在网络通信中,存在两套地址,一套是IP地址,另一套是MAC地址。

IP地址:标识计算机在网络中的唯一性。

而IP地址又分为源IP地址和目的IP地址:

  •  源IP地址:标识网络通信发起方。
  •  目的IP地址:标识网络通信的接收方。

在网络通信的报文中,其中报头包含着源IP和目的IP


1.2 端口号port和套接字socket

如上图所示,报文从用户A的计算机传送到了用户B的计算机,但是网络通信的目的就是将报文从一台计算机传送到另一台计算机吗?

将数据从计算机A传送到计算机B是手段,并不是网络通信的目的。

真正进行通信的是用户A和用户B,也就是计算机A上的某个应用程序和计算机B上的某个应用程序之间在通信

网络通信的目的就是让两台计算机上的两个进程在进行通信。

IP地址可以标识两台计算机的唯一性,但是每台计算机上会存在大量的进程,如何保证计算机A某个进程发送的数据能让计算机B指定的进程接收到呢?

换句话说,如何标识一台计算机上进程的唯一性呢?

采用端口号port来标识计算机上进程的唯一性。

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操作系统要把数据交给哪一个进程。
  • 一个端口号只能被一个进程占用。

现在我们有用来标识计算机在网络中唯一性的IP地址,又有用来标识进程在计算机中唯一性的端口号port。

  •  全网唯一进程 = IP地址(全网主机唯一性) + 端口号port(该主机上进程唯一性)

socket(套接字) = IP地址 + 端口号port。

所以要想两个进程间实现通信,必须各自有各自的套接字。


1.3 网络通信的本质

网络通信实际上是两台计算机或者多台计算机上的进程之间在通信,和我们之前Linux学习的进程间通信的区别在于进程位于不同的计算机上。

网络通信的本质:进程间通信。

  • 要实现进程间通信,必须有共享资源,而网络通信中的网络就是共享资源。
  • 网络通信其实就是在做IO,我们上网的所有行为就两种:①把数据发出去。 ②把数据读回来。

Linux下一切皆文件,所以网络在系统中也是一个"文件",也有struct结构体,也有文件描述符。

我们知道,每个进程都有一个pid来标识它在当前计算机上的唯一性,为什么网络中还需要一个端口号port来标识进程的唯一性呢?不能用pid吗?

在技术实现上是完全可以用pid的,所以就需要考虑为什么不用pid,用了端口号port?:

  • 系统是系统,网络是网络,系统使用pid,网络使用port来标识进程的唯一性,实现了系统与网络的解耦
  • 不是所有进程都提供网络服务或者网络请求的,但是所有的进程都需要pid,只有需要网络的进程才会分配一个port。
  • 客户端需要能够直接找到服务器的进程,服务器进程的唯一性不能做任何改变。

比如平时使用的QQ,手机上的QQ都是客户端,打开QQ使用都是在向服务器上的QQ进程发起网络请求,而这个服务器位腾讯公司,服务进程根据用户的网络请求再做出对应的反馈交给用户。

下载了某个应用程序以后,该程序里就绑定了服务端对应进程的IP地址和端口号。

所以使用应用程序的时候,就能精准的和服务端上对应的进程进行网络通信。

服务器的IP地址并不会随意变化,为了保证客户端每次都能找到服务端的进程,服务端的port也不能变化。  

如果使用pid来代替端口号的话,服务器每重启一次,服务进程的pid值就会改变,客户端就无法找到服务进程了。

绑定了port的进程PCB会被维护在一个哈希表中,port就是key值,操作系统能够根据key值找到对应的PCB,然后再执行它。


1.4 TCP和UDP协议

这两个协议的具体原理和细节在后面会详细讲解,这里仅需要大概了解一下特性即可。

UDP协议:(User Datagram Protocol 用户数据报协议)。

  • 传输层协议。
  • 不需要通信双方建立连接,直接发生即可。
  • 不可靠传输,可能会发生丢包等问题。
  • 面向数据报。

TCP协议:(Transmission Control Protocol 传输控制协议)。

  • 传输层协议。
  • 需要通信双方建立连接。
  • 是一种可靠传输,不会发生丢包等问题。
  • 面向字节流。

可靠和不可靠传输并没有相对的好坏,比如可靠传输付出的代价就比较大,具体这些特点是什么意思,后面会讲解到,这里只需要记住以上内容即可。


1.5 网络字节序

计算机分为大端机和小端机,不同的电脑型号就不一样,两台计算机大小端不同,接收到的数据解释出来意义也不同。

规定:网络中的字节序都采用大端

如果你的计算机是大端机,那么就可以直接向网络中发数据和从网络中接收数据,不用做转换。

如果你的计算机是小端机,那么在向网络中发送数据时,需要先将数据转换成大端,再发送到网络中。从网络中接收下来的数据,需要先转换成小端再使用。

此时就存在两个问题:

  • 自己的电脑是大端还是小端?还需要自己去判断一下。
  • 如果自己的电脑是小端,需要自己去将数据转换成大端。

这两个问题虽然我们自己能解决,但是比较繁琐,而且很容易出错,所以操作系统提供了相应的接口来进行大小端转换

主机和网络的字节序转换函数:

#include <arpa/inet.h> // 必须包含的头文件
// 主机序列转网络序列
uint32_t htonl(uint32_t hostlong); // 将主机上unsigned int类型的数据转换成对应网络字节序
uint16_t htons(uint16_t hostshort); // 将主机上unsigned short类型的数据转换成对应网络字节序// 网络序列转主机序列
uint32_t ntohl(uint32_t netlong); // 将从网络中读取的unsigned int类型的数据转换成当前计算机字节序
uint16_t ntohs(uint16_t netshort); // 将从网络中读取的unsigned short类型的数据转换成当前计算机字节序
  • 这些函数名很好记,h表示host,代表着主机,n表示network,代表着网络,s表示unit16_t,l表示uint32_t。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2. socket套接字

2.1 socket创建套接字

man socket:

返回值 int

成功返回一个int类型的值,其实就是一个文件描述符sockfd。

失败返回-1,并且设置错误码errno。

socket系统调用专门用来创建套接字,在创建的时候指定使用哪种通信协议。看看参数:

int domain

这是地址族,用来指定创建的套接字进行的是网络通信还是本地通信。

该参数可以填上图所示中的任何一个,经常使用的是AF_INET表示使用IPv4的网络套接字进行网络通信

int type

这是用来指定socket提供的能力类型,比如是面向字节流还是面向用户数据报。

该参数可以使用上图中的任何一个,其中常用的是画红色框的是面向字节流和面向用户数据报,也就是TCP和UDP。

int protocol

该参数是用来指定具体的协议名的,比如指定TCP或者DUP,但是根据前两个参数就可以确定使用哪个协议了,这个一般设置为0即可。


2.2 bind绑定

man 2 bind:

bind用来将IP地址和端口号port创建的socket套接字绑定,也就是将IP地址和端口号port和系统绑定。

返回值int

成功返回0,失败返回-1,并且设置错误码errno。

int sockfd

使用socket()返回的文件描述符sockfd,用来指定绑定哪个套接字。

const struct sockaddr * addr

struct sockaddr是一个结构体。

socklen_t addrlen

这个参数是表示sockaddr结构体大小的,单位是字节,socklen_t本质是unsigned int类型的32位变量。

其它接口:

// 开始监听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协议才会用到,后面再详细讲解。


2.3 sockaddr结构体

套接字有很多种类型,常见的有三种:

  • 网络套接字:用户跨主机之间的通信,也能支持本地通信。
  • 原始套接字:可以跨过传输层(TCP/UDP)访问底层的数据。
  • 域间套接字:只能在本地通信。

这些套接字的应用场景完全不同,所以不同种类的套接字就对应一套系统调用接口,所以三套就会对应三套不同的接口。

网络套接字:

struct sockaddr_in {short int sin_family;           // 地址族,一般为AF_INETunsigned short int sin_port;    // 端口号,网络字节序struct in_addr sin_addr;        // IP地址unsigned char sin_zero[8];      // 用于填充,使sizeof(sockaddr_in)等于16
};

通过sockaddr_in结构体,将IP地址,端口号,以及网络通信AF_INET通过系统调用bind与系统绑定,从而进行网络通信。等下我们写代码用的就是sockaddr_in结构体,用之前先清零,看个接口,man bzero:

这是一个库函数,需要包含头文件<strings.h>,该函数的作用和memset一样,不同之处在于bzero只能清零,第一个参数是目标地址,第二个参数是要清零的字节数。

在填充sockaddr_in结构体的时候,将地址类型sin_family填充为AF_INET表示网络通信。

在填充端口号sin_port的时候,需要使用htons()函数,将主机字节序转换成网络字节序,然后再进行填充。


域间套接字:

struct sockaddr_un {sa_family_t sun_family;       /* AF_UNIX */char sun_path[108];    /* 带有路径的文件名 */
};

sockaddr_un只有域间通信方式AF_UNIX以及域间通信的路径名。


设计者为了方便使用,无论是网络通信还是域间通信,都使用一套接口,通过设置不同参数来解决所有通信场景。

sockaddr_insockaddr_un是用于网络通信和域间通信两个不同的通信场景,它们的区别就在于结构体起始处的16位地址类型不同,网络通信使用AF_INET,域间通信使用AF_UNIX

但由于要使用一套接口,所以此时无论哪种通信,都使用sockaddr结构体。

  • 在填充IP地址,端口号,以及地址类型的时候,仍然是对sockaddr_in进行填充。
  • 在使用bind系统调用时,将sockaddr_in强转成sockaddr类型,在函数内部它会根据前两个字节自行判断是什么类型的通信,然后再强制转回去。

可以将sockaddr看成是基类,把sockaddr_in和sockaddr_un看出是派生类,此时就构成了多态体系。


3. UDP网络编程

网络通信一定是双方的,一端是服务端(Server)接收数据,另一端是客户端(Client)发送数据。

3.1 server的初始化服务器

我们在服务端server建个server.hpp,客户端就不建头文件了,把以前的日志拷过来,先放一部分代码:

Makefile

.PHONY:all
all:udp_client udp_serverudp_client:udp_client.ccg++ -o $@ $^ -std=c++11 -lpthread
udp_server:udp_server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udp_client udp_server

log.hpp

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)  // 可变参数
{
#ifndef DEBUG_SHOWif(level== DEBUG) {return;}
#endifchar stdBuffer[1024]; // 标准日志部分time_t timestamp = time(nullptr); // 获取时间戳// struct tm *localtime = localtime(&timestamp); // 转化麻烦就不写了snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; // 自定义日志部分va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args); // 相当于ap=nullptrprintf("%s%s\n", stdBuffer, logBuffer);// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);// fclose(fp);
}

udp_server.hpp(建议复制到VSCode跟着注释看)

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SIZE 1024class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}bool initServer() // 初始化服务器{// 从这里开始,就是新的系统调用,来完成网络功能// 1. 创建套接字(返回值是文件描述符(套接字))_sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0if (_sock < 0) // 创建套接字失败,打印日志并退出{logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}// 2. udp -> bind: 将用户设置的ip和port在内核中和我们当前的进程强关联// "192.168.110.132" -> 点分十进制字符串风格的IP地址 -> 给用户看的// 上面每一个区域取值范围是[0-255]: 1字节 -> 4个区域,理论上,表示一个IP地址,其实4字节就够了// 点分十进制字符串风格的IP地址 <-互相转化-> 4字节struct sockaddr_in local; // -> 四个字段,有一个字段清零了不用管了bzero(&local, sizeof(local)); // 清零结构体local.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数// 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络local.sin_port = htons(_port); // 考虑大小端 -> 主机序列转成网络序列,短整数// 1. 同上,先要将点分十进制字符串风格的IP地址 ->  转成4字节// 2. 4字节主机序列 -> 转成网络序列// 有一套接口,可以一次帮我们做完这两件事情,,让服务器在工作过程中,可以从任意IP中获取数据->inet_addrlocal.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){   // bind: 将ip+prot和进程强关联,参数:套接字 + 清零的结构体 + 结构体字段的长度logMessage(FATAL, "%d:%s", errno, strerror(errno)); // 小于零绑定失败就打日志和退出exit(3);}logMessage(NORMAL, "init udp server done ... %s", strerror(errno));return true;}void Start(){}~UdpServer(){}protected:uint16_t _port; // 一个服务器,一般必须需要ip地址和port(16位的整数)std::string _ip;int _sock;
};#endif

udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>static void usage(std::string proc) // usage:使用手册,proc:程序名称
{std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}// 运行服务端的方式 ./udp_server ip port // 云服务器的问题 bug
int main(int argc, char *argv[])
{if(argc != 3){usage(argv[0]);exit(1);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));svr->initServer();svr->Start();return 0;
}

udp_client.cc

#include <iostream>int main()
{return 0;
}

此时就能运行起来了。


3.2 server的数据处理Start

在预备工作做好以后,还需启动服务器,服务器进程是一个常驻内存的进程,也就是一个while(1)的死循环,在这个循环中进行网络数据的接收,处理,以及写回数据。

看看几个用到的接口,man recvfrom:

上图所示的系统调用recvfrom()用来接收网络中发过来的数据,也就是从套接字中接收。

  • 第一个参数是sockfd,是创建套接字时返回的文件描述符fd
  • 第二个参数buf是用来存储从网络中读取下来的数据的缓冲区。
  • 第三个参数是buf缓冲区的大小。
  • 第四个参数flags是读取的方式,一般设置为0,即阻塞读取数据。
  • 第五个参数sockaddr* src_addr是一个输出型参数,同样传参sockaddr_in结构体,系统会自动对这个结构体进行填充,可以获取数据的来源,包括发送方的地址类型,端口号port以及IP地址。
  • 返回值ssize_t,返回读取到的数据个数,单位是字节,如果读取失败则返回-1。

sendto() 函数是向服务器主机发送数据的:

man sendto:

  • 第一个参数sockfd是创建的套接字的文件描述符。
  • 第二个参数buf是要发送的数据所在的缓冲区。
  • 第三个参数len是要发生的数据个数,以字节为单位。
  • 第四个参数flags是发送方式,一般设置为0,表示阻塞发送。
  • 第五个参数dest_addr是存放服务器IP地址和端口号port的sockaddr_in结构体变量,在传参的时候需要强转为struct sockaddr*
  • 第六个参数,是第五个参数中结构体变量的大小,以字节为单位。

上面udp_server.hpp的Start函数:

    void Start(){// 作为一款网络服务器,永远不退出的// 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏// 目前类似echo server: client给我们发送消息,我们原封不动返回char buffer[SIZE];while(true){//  注意:peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));// 输入: peer 缓冲区大小// 输出: 实际读到的peersocklen_t len = sizeof(peer);// start. 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0) // 读取数据成功{buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串// 1. 输出发送的数据信息// 2. 输出是谁发送的信息uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);}// 分析和处理数据,TODO// end. 写回数据,类似recvfrom,后两个参数是把数据写给谁sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}

编译运行:

使用指令netstat -nuap可以查看当前服务器上的网络进程,就看见有个17602的服务器运行了。

至此服务端的工作就做完了,只要客户端发送数据,服务端就可以收到。


3.3 客户端udp_client.cc

这里在客户端就不写头文件封装了,根据上面基础,直接放代码:udp_client.cc:

#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>static void usage(std::string proc) // usage:使用手册,proc:程序名称
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}// ./udp_client 127.0.0.1 7070
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// client要不要bind?要,但是一般client不会显示的bind,程序员不会自己bind// 因为client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->// client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢?// client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)std::string message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数server.sin_addr.s_addr = inet_addr(argv[1]); // 1.先要将点分十进制字符串风格的IP地址 ->  转成4字节// 2. 4字节主机序列 -> 转成网络序列server.sin_port = htons(atoi(argv[2])); // 考虑大小端 -> 主机序列转成网络序列,短整数char buffer[1024];while (true){std::cerr << "请输入你的信息# "; // 标准错误 2打印std::getline(std::cin, message);// 下面向服务器发送消息,当client首次发送消息的时候,OS会自动给client bind他的ip和portsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));if (message == "quit")break;struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}close(sock);return 0;
}

运行客户端程序,发送数据,可以看到,客户端新收到的数据中,端口号变了,这是因为客户端的端口号是由操作系统分配的,并不是自己指定的,所以每次运行时端口号都不一样。

(此时的ip地址已经不能乱传了,上面传的是127.0.0.1是本地环回(client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中本地网络服务器的测试),常常在server中一般不自己传ip,而是设置成0,可以自己改一改,或者直接传INADDR_ANY即0.0.0.0(可以接收任意ip发来的数据),你把udp_client发给其他人,再链接阿里云的ip,就能收到其他人发的信息了,这里就不演示了)

还可以改一下udp_server.hpp的Statr,改成传命令的版本:

    void Start(){// 作为一款网络服务器,永远不退出的// 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏// 目前类似echo server: client给我们发送消息,我们原封不动返回char buffer[SIZE];while(true){//  注意:peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));// 输入: peer 缓冲区大小// 输出: 实际读到的peersocklen_t len = sizeof(peer);char result[256];std::string cmd_echo; // 读取的是指令的话// start. 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0) // 读取数据成功{buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串// 1. 输出发送的数据信息// 2. 输出是谁发送的信息// uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列// std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示// printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr){std::string err_message = "被禁止的指令";std::cout << err_message << buffer << std::endl;sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);continue;}FILE *fp = popen(buffer, "r");if (nullptr == fp){logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));continue;}while (fgets(result, sizeof(result), fp) != nullptr){cmd_echo += result;}fclose(fp);}// 分析和处理数据,TODO// end. 写回数据,类似recvfrom,后两个参数是把数据写给谁// sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);}}

到这也和上面做点处理也能让其他人操作你的机器了。


3.4 多线程收发数据

在上面的基础上对client.cc改成多线程的,把以前写的thread.hpp拷过来:

#pragma once
#include <iostream>
#include <string>
#include <cstdio>// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_tclass ThreadData // 线程数据
{
public:void *_args; // 真实参数std::string _name; // 名字
};class Thread // 封装的线程
{
public:Thread(int num, fun_t callback, void *args) : _func(callback) // 回调函数{char nameBuffer[64];snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer_name = nameBuffer;_tdata._args = args; // 线程构造时把参数和名字带给线程数据_tdata._name = _name;}void start() // 启动线程{pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据}void join() // join自己{pthread_join(_tid, nullptr);}std::string name() // 返回线程名{return _name;}~Thread() // 析构什么也不做{}protected:std::string _name; // 线程名字pthread_t _tid; // 线程tidfun_t _func; // 线程要执行的函数ThreadData _tdata; // 线程数据
};

client.cc

#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <memory>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "thread.hpp"uint16_t serverport = 0;
std::string serverip;static void usage(std::string proc) // usage:使用手册,proc:程序名称
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}// 下面的两个接口,一个线程调用一个
// 无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰
static void *udpSend(void *args) // 发送数据
{int sock = *(int *)((ThreadData *)args)->_args;std::string name = ((ThreadData *)args)->_name;std::string message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数server.sin_port = htons(serverport); // serverport和serverip是全局的server.sin_addr.s_addr = inet_addr(serverip.c_str());while (true){std::cerr << "请输入你的信息# "; //标准错误 2打印std::getline(std::cin, message);if (message == "quit")break;// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORTsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);}return nullptr;
}static void *udpRecv(void *args) // 接收数据
{int sock = *(int *)((ThreadData *)args)->_args;std::string name = ((ThreadData *)args)->_name;char buffer[1024];while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout  << buffer << std::endl;}}
}// ./udp_client 127.0.0.1 7070
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// // client要不要bind?要,但是一般client不会显示的bind,程序员不会自己bind// // 因为client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->// // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢?// // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)// std::string message;// struct sockaddr_in server;// memset(&server, 0, sizeof(server));// server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数// server.sin_addr.s_addr = inet_addr(argv[1]); //     // 1.先要将点分十进制字符串风格的IP地址 ->  转成4字节//     // 2. 4字节主机序列 -> 转成网络序列// server.sin_port = htons(atoi(argv[2])); // 考虑大小端 -> 主机序列转成网络序列,短整数// char buffer[1024];// while (true)// {//     std::cerr << "请输入你的信息# "; // 标准错误 2打印//     std::getline(std::cin, message);//     // 下面向服务器发送消息,当client首次发送消息的时候,OS会自动给client bind他的ip和port//     sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));//     if (message == "quit")//         break;//     // 下面是接收消息//     struct sockaddr_in temp;//     socklen_t len = sizeof(temp);//     ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);//     if (s > 0)//     {//         buffer[s] = 0;//         std::cout << "server echo# " << buffer << std::endl;//     }// }// 一个接收的线程,一个读取的线程serverport = atoi(argv[2]);serverip = argv[1];std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));sender->start();recver->start();sender->join();recver->join();close(sock);return 0;
}

改一下udp_server.hpp的Start,这里直接放udp_server.hpp了。

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>#define SIZE 1024class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}bool initServer() // 初始化服务器{// 从这里开始,就是新的系统调用,来完成网络功能// 一. 创建套接字(返回值是文件描述符(套接字))_sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0if (_sock < 0) // 创建套接字失败,打印日志并退出{logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}// 二. udp -> bind: 将用户设置的ip和port在内核中和我们当前的进程强关联// "192.168.110.132" -> 点分十进制字符串风格的IP地址 -> 给用户看的// 上面每一个区域取值范围是[0-255]: 1字节 -> 4个区域,理论上,表示一个IP地址,其实4字节就够了// 点分十进制字符串风格的IP地址 <-互相转化-> 4字节struct sockaddr_in local; // -> bind的第二个参数,四个字段,有一个字段清零了不用管了bzero(&local, sizeof(local)); // 清零结构体local.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数// 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络local.sin_port = htons(_port); // 考虑大小端 -> 主机序列转成网络序列,短整数// 1. 同上,先要将点分十进制字符串风格的IP地址 ->  转成4字节// 2. 4字节主机序列 -> 转成网络序列// 有一套接口inet_addr,可以一次帮我们做完这两件事情,让服务器在工作过程中,可以从任意IP中获取数据->任意IP:INADDR_ANYlocal.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){   // bind: 将ip+prot和进程强关联,参数:套接字 + 清零的结构体 + 结构体字段的长度logMessage(FATAL, "%d:%s", errno, strerror(errno)); // 小于零绑定失败就打日志和退出exit(3);}logMessage(NORMAL, "init udp server done ... %s", strerror(errno));return true;}void Start(){// 作为一款网络服务器,永远不退出的// 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏// 目前类似echo server: client给我们发送消息,我们原封不动返回char buffer[SIZE];while(true){//  注意:peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));// 输入: peer 缓冲区大小// 输出: 实际读到的peersocklen_t len = sizeof(peer);char result[256];char key[64]; // key存ip和portstd::string cmd_echo; // 读取的是指令的话// start. 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0) // 读取数据成功{buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串// 1. 输出发送的数据信息// 2. 输出是谁发送的信息uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示// printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080logMessage(NORMAL, "key: %s", key);auto it = _users.find(key);if (it == _users.end()){logMessage(NORMAL, "add new user : %s", key);_users.insert({key, peer});}// // 下面是指令版本// if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)// {//     std::string err_message = "被禁止的指令";//     std::cout << err_message << buffer << std::endl;//     sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);//     continue;// }// FILE *fp = popen(buffer, "r");// if (nullptr == fp)// {//     logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));//     continue;// }// while (fgets(result, sizeof(result), fp) != nullptr)// {//     cmd_echo += result;// }// fclose(fp);}// 分析和处理数据// end. 写回数据,类似recvfrom,后两个参数是把数据写给谁// sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);// sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);for (auto &iter : _users){std::string sendMessage = key;sendMessage += "# ";sendMessage += buffer; // 此时消息就类似:127.0.0.1-1234# 你好logMessage(NORMAL, "push message to %s", iter.first.c_str());sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&(iter.second), sizeof(iter.second));}}}~UdpServer(){if (_sock >= 0)close(_sock);}protected:uint16_t _port; // 一个服务器,一般必须需要ip地址和port(16位的整数)std::string _ip;int _sock;std::unordered_map<std::string, struct sockaddr_in> _users; // first存ip和prot,second存消息std::queue<std::string> messageQueue; // 用户层与网络的解耦->多线程->生产者消费者模型(这里就不改了)
};#endif

顺便把udp_server.cc放出来:

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>static void usage(std::string proc) // usage:使用手册,proc:程序名称
{std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}// 运行服务端的方式 ./udp_server ip port // 云服务器的问题 bug
int main(int argc, char *argv[])
{if(argc != 3){usage(argv[0]);exit(1);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));svr->initServer();svr->Start();return 0;
}

编译运行:(左边是服务端,中上是A用户发的消息,右上是A用户收到的消息,中下是B用户发的消息,右下是B用户收到的消息)

如果你把client发给其他人,就能实现类似群聊的效果了。


本篇完。

加上代码两万多字了,不过放的代码有重复的,可以自己试着敲一下。

下一篇:网络和Linux网络_3(套接字编程)TCP网络通信(多个版本)。

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

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

相关文章

如何解决3d max渲染效果图全白这类异常问题?

通过3d max渲染效果图时&#xff0c;经常会出现3Dmax渲染效果图全黑或是3Dmax渲染效果图全白这类异常问题。可能遇到这类问题较多的都是新手朋友。不知如何解决。 3dmax渲染出现异常的问题&#xff0c;该如何高效解决呢&#xff1f;今天小编这里整理几项知识点&#xff0c;大家…

打开文件 和 文件系统的文件产生关联

补充1&#xff1a;硬件级别磁盘和内存之间数据交互的基本单位 OS的内存管理 内存的本质是对数据临时存/取&#xff0c;把内存看成很大的缓冲区 物理内存和磁盘交互的单位是4KB&#xff0c;磁盘中未被打开的文件数据块也是4KB&#xff0c;所以磁盘中页帧也是4KB&#xff0c;内存…

简单理解路由重分发(用两路由器来理解)

相关命令&#xff1a; default-information originate //*重分发默认路由 redistribute rip subnets //*重分发rip redistribute ospf 1 metric 3 //*重分发ospf&#xff08;其中&#xff1a;1是ospf进程id 3是跳数&#xff09; redistribute sta…

电池故障估计:Realistic fault detection of li-ion battery via dynamical deep learning

昇科能源、清华大学欧阳明高院士团队等的最新研究成果《动态深度学习实现锂离子电池异常检测》&#xff0c;用已经处理的整车充电段数据&#xff0c;分析车辆当前或近期是否存在故障。 思想步骤&#xff1a; 用正常电池的充电片段数据构造训练集&#xff0c;用如下的方式构造…

吴恩达《机器学习》8-5->8-6:特征与直观理解I、样本与值观理解II

8.5、特征与直观理解I 一、神经网络的学习特性 神经网络通过学习可以得出自身的一系列特征。相对于普通的逻辑回归&#xff0c;在使用原始特征 x1​,x2​,...,xn​ 时受到一定的限制。虽然可以使用一些二项式项来组合这些特征&#xff0c;但仍然受到原始特征的限制。在神经网…

Unity中Shader图形流水线中的纹理

文章目录 前言一、图形流水线中的纹理1、我们的纹理一般用于&#xff1a;2、纹理的获取方式&#xff1a; 二、纹理的分类1、颜色纹理2、几何纹理 三、纹理管线四、纹理的作用1、纹理可以 替换 漫反射模型中的 漫反射系数Kd2、纹理还有的作用 前言 Unity中Shader图形流水线中的…

为什么LDO一般不用在大电流场景?

首先了解一下LDO是什么&#xff1f; LDO&#xff08;low dropout regulator&#xff0c;低压差线性稳压器&#xff09;或者低压降稳压器&#xff0c;它的典型特性就是压降。 那么什么是压降&#xff1f; 压降电压 VDO 是指为实现正常稳压&#xff0c;输入电压 VIN 必须高出 所…

Qt QWebSocket实现JS调用C++

目录 前言1、QWebChannel如何与网页通信2、QWebSocketQWebChannel与网页通信2.1 WebSocketTransport2.2 WebSocketClientWrapper2.3 初始化WebSocket服务器2.4 前端网页代码修改 总结 前言 本篇主要介绍实现JS调用C的另一种方式&#xff0c;即QWebSocketQWebChannel。与之前的…

云服务器windows service2022 部署git服务器

1 安装 下载地址gitblit 解压到你的一个目录,我这里给的是C:\gitblit 根据官网提示要下载jre or jdk7.0,这里建议使用下载jre (jdk 有时候运行出问题,或者2个都安装),自行安装java,这里不做环境配置的说明 ==================================== 进入c:\gitblit\data 目录里面…

Docker - 企业项目

Docker - 企业项目 因为环境原因&#xff0c;本章本人没有实际操作&#xff0c;以理论为主 容器单独没有什么意义&#xff0c;有意义的是容器的编排 Docker 4台&#xff1a;1核2G的ECS K8s 9台&#xff1a;2核4G的ECS Docker Compose Docker Swarm # manager节点初始化sw…

场景图形管理-多视图多窗口渲染示例(4)

多视图多窗口渲染示例的代码如程序清单8-6所示 // 多视图多窗口渲染示例 void compositeViewer_8_6(const string &strDataFolder) {// 创建一个CompositeViewer对象osg::ref_ptr<osgViewer::CompositeViewer> viewer new osgViewer::CompositeViewer();// 创建两个…

时间序列预测实战(十五)PyTorch实现GRU模型长期预测并可视化结果

往期回顾&#xff1a;时间序列预测专栏——包含上百种时间序列模型带你从入门到精通时间序列预测 一、本文介绍 本文讲解的实战内容是GRU(门控循环单元)&#xff0c;本文的实战内容通过时间序列领域最经典的数据集——电力负荷数据集为例&#xff0c;深入的了解GRU的基本原理和…

Spring Framework 简介与起源

Spring是用于企业Java应用程序开发的最流行的应用程序开发框架。全球数百万开发人员使用Spring Framework创建高性能、易于测试和可重用的代码。 Spring Framework是一个开源的Java平台。它最初由Rod Johnson编写&#xff0c;并于2003年6月在Apache 2.0许可下首次发布。 Spri…

使用 Java 枚举和自定义数据类型

介绍 在 Java 编程领域&#xff0c;理解并有效利用枚举和自定义数据类型对于编写健壮、可维护且高效的代码至关重要。这篇文章旨在深入研究 Java 枚举和自定义数据类型的概念&#xff0c;提供见解和示例&#xff0c;以增强您的编码技能和知识。 理解 Java 中的枚举 枚举是枚…

C语言对10个数进行排序,使用快速排序算法

完整代码&#xff1a; // 对10个数进行排序&#xff0c;使用快速排序算法 #include<stdio.h>//用第一个元素将待排序序列划分成左右两个部分&#xff0c;返回排序后low的位置&#xff0c;即枢轴的位置 int partition(int arr[],int low,int high){//让待排序序列中的第一…

Android修行手册-Gson中不用实体类生成JsonObject或JsonArray

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列点击跳转>ChatGPT和AIGC &#x1f449;关于作者 专…

9.程序的机器级代码表示,CISC和RISC

目录 一. x86汇遍语言基础&#xff08;Intel格式&#xff09; 二. AT&T格式汇编语言 三. 程序的机器级代码表示 &#xff08;1&#xff09;选择语句 &#xff08;2&#xff09;循环语句 &#xff08;3&#xff09;函数调用 1.函数调用命令 2.栈帧及其访问 3.栈帧的…

狂神说笔记 快速入门Nginx

公司产品出现瓶颈&#xff1f; 我们公司项目刚刚上线的时候&#xff0c;并发量小&#xff0c;用户使用的少&#xff0c;所以在低并发的情况下&#xff0c;一个jar包启动应用就够了&#xff0c;然后内部tomcat返回内容给用户。 但是慢慢的&#xff0c;使用我们平台的用户越来…

逻辑回归-癌症病预测与不均衡样本评估

1.注册相关库(在命令行输入&#xff09; pip install scikit-learn pip install pandas pip install numpy 2.导入相关库 import pandas as pd import numpy as np from sklearn.metrics import classification_report from sklearn.model_selection import train_test_split…

黑马点评回顾 redis实现共享session

文章目录 传统session缺点整体访问流程代码实现生成验证码登录 问题具体思路 传统session缺点 传统单体项目一般是把session存入tomcat&#xff0c;但是每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat&#xff0c;并且把自己的信息存放到第一台服务器…