网络套接字1
📟作者主页:慢热的陕西人
🌴专栏链接:Linux
📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言
本博客主要内容讲解了udp的Linux环境下的使用,以及网络的一些基础知识
文章目录
- 网络套接字1
- 1.预备知识
- 1.1理解源IP地址和目的IP地址
- 1.2认识端口号
- 1.3理解"端口号"和"进程ID"
- 1.4理解源端口号和目的端口号
- 1.5认识TCP协议
- 1.6认识UDP
- 1.7网络字节序
- 2.socket编程接口
- 2.1socket常见API
- 2.2sockaddr结构
- 3.实现一个C/S示例代码
- 3.1V1
- 3.2V2
- 3.3V3
- 3.4V4版本
- 3.5提供一个Windows客户端
- 3.6客户端和服务端的源码:
1.预备知识
1.1理解源IP地址和目的IP地址
唐僧例子1
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址 .
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析 。
1.2认识端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数 ;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理 ;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用;
1.3理解"端口号"和"进程ID"
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这
两者之间是怎样的关系 ?
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
1.4理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要
发给谁”;
1.5认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题
- 传输协议层
- 有连接
- 可靠传输
- 面向字节流
1.6认识UDP
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.7网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节 ;
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#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);
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如
htonl
表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
2.socket编程接口
2.1socket常见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);
2.2sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
- 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
结构体指针做为参数。
3.实现一个C/S示例代码
3.1V1
准备工作:网络套接字的创建和对应的端口绑定
void InitServer(){//1.创建socket接口,打开网络文件//数据报套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);//返回的是一个网络文件的文件描述符if(sock_ < 0){std::cerr << "create socket error" << strerror(errno) << std::endl;exit(SOCKET_ERR); //用枚举构造的一个退出码}std::cout << "create socket success:" << sock_ << std::endl;//2.给服务器指明IP地址和端口号portstruct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里bzero(&local, sizeof(local)); //将local初始化为0//对local进行赋值local.sin_family = AF_INET; //指定为Address_Familylocal.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序// inet_addr: 1,2两个功能都具备// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化// 2. 需要将主机序列转化成为网络序列// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IPif(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0) //绑定端口号{std::cerr << "bind socket error" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket success:"<< sock_ << std::endl;}
v1版本中我们发现我们的服务端在绑定的时候却出错了!这是为什么呢?
云服务器不需要,bind对应的ip地址,需要让服务器自己指定IP地址; 我们自己本地的虚拟机或者物理机是支持的。
[mi@lavm-5wklnbmaja udpv1]$ ./udp_server
server addr1.1.1.1:8082
create socket success
bind socket errorCannot assign requested address
我们如此去处理:
并且因为INADDR_ANY是一个缺省的零值,我们并不需要对他进行主机转网络化。
// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,
local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IP
接下来完成收发消息:
收消息的接口:
ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);
socket
:这是已建立连接的套接字文件描述符,用于接收数据。
buffer
:这是指向用于存储接收数据的缓冲区的指针。
length
:指定接收数据的最大长度。
flags
:用于控制接收数据的方式。常见选项包括:
MSG_WAITALL
:阻塞等待,直到接收到指定长度的数据。MSG_DONTWAIT
:非阻塞模式,如果没有数据可读,立即返回-1。
address
:这是指向struct sockaddr
结构的指针,用于存储发送方的地址信息。
address_len
:表示address
结构的字节数。需要注意的是,
recvfrom
函数在 UDP 协议中返回长度为 0 的数据是可行的。因为在 UDP 的情况下,它会形成 20 字节的 IP 首部(IPv4)和一个 8 字节的 UDP 首部,而没有数据的 IP 数据报。因此,UDP 是无连接的协议,不需要真正的发送缓冲区。
发消息的接口:
ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
sendto
函数用于将数据从套接字发送给目标主机。
socket
:这是已建立连接的套接字文件描述符,用于发送数据。
message
:这是指向要发送数据的缓冲区的指针。
length
:指定要发送数据的长度。
flags
:用于控制发送数据的方式。常见选项包括:
MSG_DONTWAIT
:非阻塞模式,如果无法立即发送数据,立即返回-1。
dest_addr
:这是指向struct sockaddr
结构的指针,用于指定目标主机的地址信息。
dest_len
:表示dest_addr
结构的字节数。需要注意的是,
sendto
函数在 UDP 协议中不需要建立连接,因此不需要经过连接操作。它专门为 UDP 协议提供,因此参数中包含了目标地址信息。
我们完成服务端的收消息和发消息模块:
void Start(){char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节while(true){//收消息struct sockaddr_in peer;socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0if(n > 0) buffer[n] = '\0'; //读取成功,设置\0else continue; //否则的话继续读取//拿到地址和端口,并且完成网络转主机化std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port); std::cout <<clientip << "-"<< clientport << "#get massege : " << buffer << std::endl;//发消息sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, sizeof(peer));}}
我们运行以后,可以用netstat -nuap
的指令去查看我们对应的当前在Linux主机上运行的网络服务:
[root@lavm-5wklnbmaja test_db]# netstat -nuap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:68 0.0.0.0:* 2658/dhclient
udp 0 0 172.16.0.3:123 0.0.0.0:* 1845/ntpd
udp 0 0 127.0.0.1:123 0.0.0.0:* 1845/ntpd
udp 0 0 0.0.0.0:123 0.0.0.0:* 1845/ntpd
udp 0 0 172.16.0.3:47847 169.254.169.240:53 ESTABLISHED 11484/nscd
udp 0 0 0.0.0.0:8081 0.0.0.0:* 29515/./udp_server
udp6 0 0 fe80::d662:97b7:397:123 :::* 1845/ntpd
udp6 0 0 ::1:123 :::* 1845/ntpd
udp6 0 0 :::123 :::* 1845/ntpd
完成对应的客户端:
static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip serverport "<< "port\n" << endl;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1]; //获取ipuint16_t serverport = atoi(argv[2]); //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式while(true){//用户输入消息string message;cout << "Please Enter# ";cin >> message;//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));//收char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if(n > 0) // 收到了数据{buffer[n] = '\0';cout << "server echo# " << buffer << endl;}}return 0;
}
这里我们进行远端测试的时候,要先打开自己服务器上udp的端口防火墙,要不然会测试失败。
3.2V2
我们的v1版本实现的是网络的IO问题,那么我们接下来要实现的是要对我们服务器收到的消息进行业务处理
1.做字符串处理,小写转大写
std::string transactionString(std::string requets)
{std::string result;char c;for(auto& r : requets){if(islower(r)){c = toupper(r);result.push_back(c);}elseresult.push_back(r);}return result;
}
2.做命令处理
在本地把命令发给服务器,让服务器在本地执行,并且把执行结果返回给客户端
这里我们需要用到popen
接口:
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen(const char *command, const char *type)
:
- 参数:
command
:一个字符串,表示要执行的命令或可执行程序的名称。type
:一个字符,指定打开方式。它只能是'r'
或'w'
,分别表示读取命令的标准输出或写入命令的标准输入。- 功能:
popen()
函数创建一个管道,并通过fork
或者启动一个子进程来执行指定的command
。- 如果
type
是'r'
,则从应用程序的标准输出读取内容并返回一个文件指针。- 如果
type
是'w'
,则将数据传递给应用程序的标准输入。popen()
的返回值是一个文件指针,成功时非空,失败时为NULL
。pclose(FILE *stream)
:
- 参数:
stream
:先前由popen()
返回的文件指针。- 功能:
pclose()
用于关闭由popen()
创建的管道和文件指针。- 成功时,返回子进程的终止状态;失败时,返回
-1
,错误原因存储在errno
中。
bool isPass(const std::string& command)
{ bool pass = true;auto pos = command.find("mv");if(pos != std::string::npos) pass = false;pos = command.find("rm");if(pos != std::string::npos) pass = false;// pos = command.find("");// if(pos != std::string::npos) pass = false;return pass;
}std::string excuteCommend(const std::string& command)
{//1.安全检查if(!isPass(command)) return "Bad command!";//2.业务处理FILE* fp = popen(command.c_str(), "r");if(fp == nullptr) return "None"; //3.获取结果char line[1024];std::string result;while(fgets(line, sizeof(line), fp) != NULL){result += line;}pclose(fp);return result;
}
3.3V3
将我们服务器收到的每一个消息,发送给每一个服务端,模拟一个群聊的功能:
那么过程中有人收消息,有人发消息,类似于一个cp模型:所以我们直接使用之前使用过的一个环形队列的模型;
那么为了维护唤醒队列的线程安全,我们引入之前实现的LockGuard
同时我们引入了Thread.hpp
思路:主要的思路是,每当一个客户端向服务端发送消息的时候,
发的进程:先在map内部去检测,查看当前的用户是否在map内部,如果不在,我们就push到map,然后将消息插入到我们的环形队列中。
收的进程:先从环形队列中取出收到的消息,然后将map中的peer去出放在一个vector中(这个过程要保证线程安全),最后在遍历vector,将消息广播出去,也就是发给曾经给服务器发过消息的客户端。
//RingQueue.hppconst int N = 50;#include<vector>
#include<semaphore.h>using namespace std;template<class T>
class RingQueue
{private://P操作void P(sem_t &m){sem_wait(&m);}//V操作void V(sem_t &m){sem_post(&m);}void lock(pthread_mutex_t &m){pthread_mutex_lock(&m);}void unlock(pthread_mutex_t &m){pthread_mutex_unlock(&m);}
public:RingQueue(int num = N):_cap(num),_v(num) ,_start_step(0), _end_step(0){sem_init(&_data_sem, 0, 0); //数据初始量为零sem_init(&_space_sem, 0, _cap); //空间初始量为_cappthread_mutex_init(&_con_mutex, nullptr);pthread_mutex_init(&_pro_mutex, nullptr);}void push(const T& in){P(_space_sem); //p操作之后不需要判断是否有资源,因为p操作通过之后一定有资源lock(_pro_mutex);_v[_end_step++] = in;_end_step %= _cap;unlock(_pro_mutex);V(_data_sem);}void pop(T* out){P(_data_sem);lock(_con_mutex);*out = _v[_start_step++];_start_step %= _cap;unlock(_con_mutex);V(_space_sem);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_con_mutex);pthread_mutex_destroy(&_pro_mutex);}private:vector<T> _v; //用于模拟环形队列sem_t _data_sem; //表示数据的信号量sem_t _space_sem; //表示空间的信号量int _cap; //表示环形队列的大小int _start_step; //表示消费者的位置int _end_step; //表示生产者的位置pthread_mutex_t _con_mutex;pthread_mutex_t _pro_mutex;};//LockGuard.hpp#pragma#include<mutex>class Mutex
{
public:Mutex(pthread_mutex_t* pthread) : _pthread(pthread){}void lock(){pthread_mutex_lock(_pthread);}void unlock(){pthread_mutex_unlock(_pthread);}~Mutex(){}
private:pthread_mutex_t * _pthread;};class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex) :_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}private:Mutex _mutex;
};//Thread.hpp#pragma once#include<pthread.h>
#include<unistd.h>
#include<string>class Thread
{public:typedef enum{NEW = 0,RUNING,EXIT}ThreadStatus;// typedef void* (*func_t)(void*);using func_t = std::function<void()>;Thread(int mum, func_t func) :_tid(0), _status(NEW), _func(func){char name[128];snprintf(name, sizeof name, "thread-%d", _tid);_name = name;}int status() { return _status; }string thread_name(){ return _name; }pthread_t threadid(){if(_status == RUNING){return _tid;}else return 0;}static void* runHelper(void* args){Thread* ts = (Thread*) args; //通过这种方式拿到对象(*ts)();return nullptr;}void operator()(){if(_func != nullptr) _func(); //使用仿函数的方式运行对应的线程进入函数}void run(){int n = pthread_create(&_tid, nullptr, runHelper, this);if(n != 0) exit;_status = RUNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join error:" << _name << endl;return ;}_status = EXIT;}~Thread(){}public:pthread_t _tid;string _name;func_t _func; //线程未来要执行的回调ThreadStatus _status;
};
3.4V4版本
客户端线程化:构造一个线程专门去执行们收消息的任务
//udp_client.cc#include"udp_client.hpp"#include<pthread.h>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip serverport "<< "port\n" << endl;
}void* reciver(void* args)
{int sock = *(static_cast<int *>(args)); // 获取到对应的套接字while(true){// 收char buffer[2048];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n > 0) // 收到了数据{buffer[n] = '\0';cout << buffer << endl;}}return nullptr;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1]; //获取ipuint16_t serverport = atoi(argv[2]); //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式pthread_t tid;pthread_create(&tid, nullptr, &reciver, &sock);while(true){//用户输入消息string message;std::cerr << "Please Enter Your Message#";//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}pthread_join(tid, nullptr);return 0;
}
3.5提供一个Windows客户端
我们这里实现一个windows的客户端,最终实现之后可以和Linux端一同通信。
代码只有四处不同:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<string>
#include<winsock2.h>#pragma comment(lib, "ws2_32.lib") //引入windows下的库using namespace std;int main()
{//用于检查WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){printf("init error");return -1;}//....//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏WSACleanup();return 0;
}
完整代码:
#include<iostream>
#include<string>
#include<winsock2.h>
#include <WS2tcpip.h> // for inet_pton
#include <thread>#pragma comment(lib, "ws2_32.lib") //引入windows下的库using namespace std;int64_t serverport = 8888;string serverip = "117.72.37.100";void ReceiveData(SOCKET sock) {char buffer[2048];sockaddr_in temp;int len = sizeof(temp);while (true) {int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);if (n > 0) {buffer[n] = '\0';cout << buffer << endl;}}
}int main()
{//用于检查WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){printf("init error");return -1;}int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "create socket error " << std::endl;exit(-2);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server//server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));thread receiveThread(ReceiveData, sock);cout << "Please Enter Your Message#";while (true){//用户输入消息string message;//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}receiveThread.join();//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏WSACleanup();return 0;
}
3.6客户端和服务端的源码:
- server.hpp
#pragma once#include<iostream>
#include<cstring>
#include<strings.h>
#include<string>
#include<cstdio>
#include<functional>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unordered_map>
#include<pthread.h>
#include<vector>
#include"err.hpp"
#include"RingQueue.hpp"
#include"LockGuard.hpp"
#include"Thread.hpp"namespace ns_server
{const static uint16_t default_port = 8080; //设定一个端口号的默认值using func_t = std::function<std::string(std::string)>; //使用函数包装器,包装一个返回值和参数的都是string类型的函数class UdpServer{public:UdpServer(uint16_t port = default_port):port_(port){std:: cout << "server addr" << port_ << std::endl;pthread_mutex_init(&lock, nullptr);p = new Thread(1, std::bind(&UdpServer::Recv, this));c = new Thread(2, std::bind(&UdpServer::Broadcast, this));}void start(){//1.创建socket接口,打开网络文件//数据报套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);//返回的是一个网络文件的文件描述符if(sock_ < 0){std::cerr << "create socket error" << strerror(errno) << std::endl;exit(SOCKET_ERR); //用枚举构造的一个退出码}std::cout << "create socket success:" << sock_ << std::endl;//2.给服务器指明IP地址和端口号portstruct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里bzero(&local, sizeof(local)); //将local初始化为0//对local进行赋值local.sin_family = AF_INET; //指定为Address_Familylocal.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序// inet_addr: 1,2两个功能都具备// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化// 2. 需要将主机序列转化成为网络序列// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IPif(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0) //绑定端口号{std::cerr << "bind socket error" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket success:"<< sock_ << std::endl;p->run();c->run();}void addUser(const std::string& name, const struct sockaddr_in& peer){LockGuard lockguard(&lock);auto iter = onlineuser.find(name);if(iter == onlineuser.end())onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name , peer));}void Recv(){char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节while(true){//收消息struct sockaddr_in peer;socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0if(n > 0) buffer[n] = '\0'; //读取成功,设置\0else continue; //否则的话继续读取//拿到地址和端口,并且完成网络转主机化std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port); std::cout <<clientip << "-"<< clientport << "#" << buffer << std::endl;//构建一个用户并检查std::string name = clientip;name += "-";name += std::to_string(clientport);//如果不存在,就插入,如果存在什么都不做addUser(name, peer);std::string message = name + ">> " + buffer;rq.push(message);// //做业务处理// std::string reponse = service_(buffer);// //发消息// sendto(sock_, reponse.c_str(), reponse.size() , 0, (struct sockaddr *)&peer, sizeof(peer));}}void Broadcast(){while (true){std::string sendstring;rq.pop(&sendstring);std::vector<struct sockaddr_in> v; // 这里用vector优化一下效率,因为vector最终只在内存中拷贝数据{LockGuard lockguard(&lock);for (auto user : onlineuser){v.push_back(user.second);}}for (auto user : v){sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&user, sizeof(user));}}} ~UdpServer() {pthread_mutex_destroy(&lock);c->join();p->join();delete c;delete p;}private:int sock_;uint16_t port_;//func_t service_; //std::unordered_map<std::string, struct sockaddr_in> onlineuser;RingQueue<std::string> rq;pthread_mutex_t lock;Thread *c;Thread *p;// std::string ip_;};}
- server.cc
#include<iostream>
#include<memory>
#include<string>#include"udp_server.hpp"using namespace std;
using namespace ns_server;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "port\n" << endl;
}//上层的业务处理, 不关心网络, 只负责业务处理
std::string transactionString(std::string requets)
{std::string result;char c;for(auto& r : requets){if(islower(r)){c = toupper(r);result.push_back(c);}elseresult.push_back(r);}return result;
}bool isPass(const std::string& command)
{ bool pass = true;auto pos = command.find("mv");if(pos != std::string::npos) pass = false;pos = command.find("rm");if(pos != std::string::npos) pass = false;// pos = command.find("");// if(pos != std::string::npos) pass = false;return pass;
}std::string excuteCommend(const std::string& command)
{//1.安全检查if(!isPass(command)) return "Bad command!";//2.业务处理FILE* fp = popen(command.c_str(), "r");if(fp == nullptr) return "None"; //3.获取结果char line[1024];std::string result;while(fgets(line, sizeof(line), fp) != NULL){result += line;}pclose(fp);return result;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGE_ERR);}//通过命令行参数获取端口号uint16_t port = atoi(argv[1]);// unique_ptr<UdpServer> server(new UdpServer("1.1.1.1", 8082));unique_ptr<UdpServer> server(new UdpServer(port));server->start();// server->Start();return 0;
}
- client.hpp
#pragma once#include<iostream>
#include<memory>
#include<string>
#include<cstring>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>#include"err.hpp"
- client.cc
#include"udp_client.hpp"#include<pthread.h>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip serverport "<< "port\n" << endl;
}void* reciver(void* args)
{int sock = *(static_cast<int *>(args)); // 获取到对应的套接字while(true){// 收char buffer[2048];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n > 0) // 收到了数据{buffer[n] = '\0';cout << buffer << endl;}}return nullptr;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1]; //获取ipuint16_t serverport = atoi(argv[2]); //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式pthread_t tid;pthread_create(&tid, nullptr, &reciver, &sock);while(true){//用户输入消息string message;std::cerr << "Please Enter Your Message#";//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}pthread_join(tid, nullptr);return 0;
}
到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正