一.Mac地址,IP地址,端口地址
1.1 IP地址
在网络中我们存在很多台主机(电脑),如何指示我这台主机发出的消息,到底是发给哪一台主机的呢?
IP地址的概念也就被引出,每一台电脑在网络中都有唯一的一个IP地址,它的作用就是来指示消息要往哪里发的问题,用来标识网络中不同主机的地址
IP地址的格式分为IPV4,IPV6,两者的主要区别,主要在于IPV4,用4个字节,也就是32位的整数来表示,而IPV6则是用6个字节,也就是128位来表示。
在这篇博客中,凡是提到IP协议, 没有特殊说明的, 默认都是指IPv4
在我们电脑设置,网络配置中,我们也可以找到我们电脑自己的IP地址

我们通常使用 “点分十进制” 的字符串表示IP地址, 例如 192.168.0.1 ;
用点分割的每一个数字表示一个字节, 范围是 0 - 255;
理论上,2的32次方,可以表示总共42亿多不同的IP地址,这已经是一个非常庞大的数字,所以最初设计的时候,IP地址的格式设计为IPV4,但现在全球人口都已经70多亿,IPV4已经不足以适应人数的需求变化,所以IPV6应运而生.
对于发送消息的一方的IP地址,我们称之为源IP地址
对于接受消息的一方的IP地址,我们称之为目的IP地址
1.2 MAC地址
但是仅仅有IP地址,还不能保证两部主机进行跨网络通讯。
在实际消息发送过程当中,消息是要经过一个个节点(路由器)进行转发的,而不是一口气直接送到目标主机。
这就好比我们小时候看过的西游记,唐僧四人从长安出发,去西天取经。
长安就是他们的源IP地址,西天就是他们的目的IP地址。
但师徒四人并非直接一步就到达西天,而是途径了很多个国家,这些国家就是一个个的节点,比如说车迟国,女儿国等等,这就是MAC地址。
当师徒四人到达一个新的国家,对应的MAC地址就会发生改变,MAC地址实际只在当前局域网内有效
类似IP地址,MAC地址同样存在源MAC地址,目的MAC地址的说法。
比如唐僧四人到达乌鸡国,下一站要去车迟国
那此时源MAC地址就是乌鸡国,目的MAC地址就是车迟国
当到达车迟国后,师徒四人的下一站是通天河
那此时源MAC地址就变为车迟国,目的MAC地址就是通天河
同样的道理,当经过不同节点(路由器),MAC地址都会发生改变,然后消息就在一个个节点中完成转发,最终到达目的主机,它的作用就是用来识别数据链路层中相连的节点;
一般长度为48位, 也就是6个字节.
一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
MAC地址通常是全球唯一的,在网卡出厂时就确定了, 不能修改.
但也有一些特殊情况,比如虚拟机中的MAC地址不是真实的MAC地址, 可能会冲突; 也有些网卡支持用户配置MAC地址
但总体而言,我们把它简单理解为网卡与MAC地址一一对应,是没有问题的。
同样的,假如我们用的是windows电脑,在查看硬件和连接属性地方,我们同样可以看到我们的MAC地址(物理地址)是多少
假如用的是linux系统,则输入ifconfig指令,同样可以查看对应的MAC地址是多少

我们用到的大部分局域网都是以太网标准,其中ether对应就有”以太“的意思,而ether后面的这个地址就是当前云服务器所对应的MAC地址。
但实际云服务器上的MAC地址可能不是真正的MAC地址,该MAC地址可能模拟出来的
1.3 端口地址
但是光有IP地址,确定消息要发给谁;有MAC地址,用来识别传输路上的一个个节点,这也还不足够。
传输并不是我们的最终目的,我们的目的是要对传过来的数据进行对应的处理和响应
换句话说,通过IP地址我们可以标识互联网上唯一的一台主机,并将数据发送到目标主机,这是我们的手段,但我们真正想要的是目标主机给我们提供的某种服务,比如说淘宝服务器端提供的购买支付服务等等。
更进一步讲,什么是服务?不就是一个个的task(任务)吗?
所以,什么是网络间通信呢?
本质就是进程间通信
我们点开某一款我们经常使用的app,它其实就是一段二进制代码,是手机操作系统(安卓,mac)管理的一个进程;对应的服务器端,也是一段二进制代码,死循环在那运行着,也是OS管理的一个进程
两个进程通过网络进行通讯,给用户提供对应的服务,这就是网络通信的本质。
但如何标识我们用户,对应的服务器端中的某一个进程呢?
用户可以同时打开多个app,服务器端也并不是只服务你某一个人,在用户端,服务器端都是存在多个进程的,假如想要两个进程通信,标识区分不同进程是非常重要的事情!
这就是端口地址!!!它的作用就是标识区分主机上不同的进程
问题1
有人可能会疑问,每一个进程不是都有对应的进程pid吗?用pid进行标识不也可以吗?为什么要单独再设计一个端口号的概念呢?
1.不是所有的进程都要进行网络通信,假如用PID来标识,就很难进行区分,这个进程到底要不要进行网络通信呢?
2.解耦,进程PID还牵连到进程管理,我们希望的是进程管理,与网络两者可以区分开来,自成一套体系
Lg.这就好像我们每个人都有自己的身份证号,用来标识我们的唯一性,但我们在学校也有自己的学号!它也可以用来标识我们在学校的唯一性,对学号的处理,并不会改变我们的身份证号,学校管理和国家进行行政管理,这是两套不同的管理体系,互不干扰。
它的基本格式是一个2字节16位的整数,属于传输层协议的内容。
问题2
2.1一个端口号可以被多个进程绑定吗?
不允许!!!
那就没有区分不同进程这一说法了
2.2一个进程可以关联多个端口号吗?
可以!!!
这主要是能够实现多路复用的功能,通过关联多个端口号,一个进程可以同时监听或处理多个网络连接。这种多路复用的机制允许一个进程同时与多个客户端通信,而无需为每个连接创建一个独立的进程
问题3:
底层如何通过port找到对应进程的?
采用哈希的方式建立了端口号和进程PID或PCB之间的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程
通俗的讲,它就好像一个大的数组,数组的下标就是一个个的端口号,我们可以往里面填充不同的PCB,两者就建立起关联!
由于下标具有唯一性,所以一个端口号,只能被一个进程绑定
但是不同下标,可以放相同的PCB吗?答案是可以的,即一个进程可以关联多个端口号
于是, IP+Port(端口地址)的组合,标识了互联网中唯一的一个进程
或者更严谨的说法,通过IP + Port构建进程唯一性,来实现基于网络的进程间通信的方法,我们称之为socket套接字。
回忆:
进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有我们今天将要学的套接字,只不过前者是不跨网络的,而后者是跨网络的
二.TCP/UDP
在上一节,我们已经提到过,socket套接字定义了通信的一端,它的本质就是通过IP+Port端口号的方式,来标识进程唯一性,从而实现网络间进程通讯。
和管道,信号量等等,socket套接字也提供了很多接口,使得程序员可以使用一组抽象的函数或方法来进行网络通信,而无需关注底层的网络细节。
在网络编程中,socket套接字通常分为两种类型:
流套接字(Stream Socket): 也称为TCP(Transmission Control Protocol)套接字,使用面向连接的可靠的字节流进行通信。
三大特点:
1.有连接(两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输)
2.可靠(数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法)
3.有序(数据按顺序依次传递)
如何理解流的含义呢?
它就好像我们生活中的自来水,最后要多少水,完全取决于用户的需求,只要用户提出对应的要求,最后就会有相应的内容送达。
数据报套接字(Datagram Socket): 也称为UDP(User Datagram Protocol)套接字,使用无连接的、不可靠的数据报进行通信。
三大特点:
1.无连接
2.不可靠(数据传送过程中假如出现乱序,丢包等等,UDP协议不会进行相应的处理,它只负责将数据传输过去)
3.无序
TCP套接字,或者说UDP套接字,两者之间并无优劣之分,不能看到说可靠,就全部选择TCP,要结合具体场景进行分析。
假如要可靠,稳定,数据传输一定要准确无误,那肯定不管设计复杂,也要选择TCP,类似于银行转钱,假如转钱的时候,10000块,少了几个0,没有成功传输,那造成的后果是无法想象的!
但假如没有太高的要求,对于一些对实时性要求较高、数据丢失可以接受的场景,则用UDP协议明显更适合,毕竟它简单!
注意: 一些网站在设计网络通信算法时,会同时采用TCP协议和UDP协议,当网络流畅时就使用UDP协议进行数据传输,而当网速不好时就使用TCP协议进行数据传输,此时就可以动态的调整后台数据通信的算法
三.UDP套接字程序编写
3.1明确目标
现在,我们就要进一步通过实际代码的方式,来加深对上述知识的理解。
首先,我们的目标是这样的,分别通过socket套接字的方式编写两段程序,一段程序是服务器端程序,另一段程序是用户端程序
使两个进程成功进行交互通信

在我们实际生活中,也是如此
比如QQ这个软件,腾讯的程序员们,首先编写好对应的用户端和服务器端程序
我们作为用户,能够在对应的软件商店,下载下来对应的用户端程序,下载完毕后,其实我们的手机中,就存在对应的用户端代码,只要我们点开QQ这个图标,对应的进程就会被创建。
而在腾讯服务器上,服务器端的程序会不断运行,和用户端进程建立网络间通讯,给用户提供不同的服务!
我们的目标是全双工通讯
服务器端进程,可以收到用户端的消息,也能向用户端发消息
同样的,用户端也可以接受来自服务器端的消息,也能有向服务器端发送消息的能力。

3.2 socket编程接口(系统调用)
和我们之前学过的文件操作,多进程操作,多线程操作等等,我们现在所学的知识,都属于系统调用接口API层面,通过学习这些API接口(大佬帮我们写好的函数),我们可以访问OS操作系统,通过OS来调用驱动层程序,进而操作不同硬件,电脑不就运行起来了吗?
这次我们所学的API接口,最后针对的硬件就是网卡,通过网卡,我们可以让我们的二进制消息,通过有线,电磁波等等的方式(物理层),发送出去。

创建套接字:(TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol);
绑定端口号:(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
监听套接字:(TCP,服务器)
int listen(int sockfd, int backlog);
接收请求:(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
建立连接:(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.2.1 sockaddr结构体
在第一节我们已经提到了,IP+Port端口号的结合,保证了网络中进程的唯一性,从而可以实现两个进程可以相互通信
那我们实际调用API接口的时候,是IP,Port端口号,IPV4协议等等,分别作为我们的参数,来进行调用的吗?
答案是:不是!
想想也知道,这不可能,那函数的参数要有多长啊!
实际上,我们假如要调用相应socket套接字提供的API接口,我们用户需要填充的是sockaddr结构体

更准确来说,总共有三种结构体,分别是sockaddr结构体,sockaddr_in结构体还有sockaddr_un结构体
其中,sockaddr_in结构体,用于网络间进程通信
sockaddr_un结构体,用于本地间进程通信(没错,你没看错,socket套接字还能发挥像信号量,共享内存,消息队列一样的功能,本地不同进程进行通信!!!其实仔细想想,好像也蛮合理对吧)
不过我们调用API接口的时候(像是我们的socket,bind函数等等),它们用的参数都是sockaddr*的参数,它的作用其实就类似于用C方式实现C++的多态,对于不同的传参,会在底层进行相应的判断
假如判断出传进来的是sockaddr_in结构体,那就调用网络间进程通信的方法
假如判断出传进来的是sockaddr_un结构体,那就调用本地间进程通信的方法
对于不同的对象,调用相同的系统调用函数socket,bind等等,会有不同的响应,这不正是多态吗?
为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体
下层识别的方式也很简单,就是看结构体的头16位
我们一般称之为协议家族sin_family,其中sin不是我们所想的sin三角函数,而是socket internet取头几个字母组合而成
假如填充的是AF_INET,那就是要进行网络通信
假如填充的是AF_UNIX,那就是要进行本地通信
接下来,我们具体看看我们接下来要用的sockaddr_in结构体代码

第一个参数是__SOCKADDR_COMMON,看上去蛮玄乎,不知道是什么
但实际上转到定义,发现它实际就是一个宏定义,将括号内传进去的参数和family拼接起来,作为参数名字,类型是sa_family_t(unsigned short int),其实就是我们前面提到过的协议家族,用来指定地址类型,调用系统接口时,告诉下层,接下来要网络通信还是本地通信.

PS:IPv4和IPv6的地址格式定义在netinet/in.h中,头文件记得包含
第二个参数可以看到sin_port,就是我们的端口号
第三个参数sin_addr(socket internet address)就是我们的IP地址
不要看它是一个结构体,但实际上结构体里面就一个32字节的变量

第四个参数是字符数组,它的目的是为了使sockaddr_in结构体还有sockaddr_un结构体,这两个结构体的大小与早期版本的 struct sockaddr 保持一致,以确保在使用早期的套接字接口函数时,这些结构体的大小一致。这种填充方式对于确保二进制兼容性非常重要,因为这样旧的代码和库可以在不需要修改的情况下与新的结构体一起使用
当然在实际使用的时候,其实我们涉及并不多?直接全部初始化为0即可.
问题1.
为什么要有这么多进程间通信方式?
又有管道、消息队列、共享内存、信号量等方式了,现在套接字这里又出现了可以用于本地进程间通信的域间套接字,为什么会有这么多通信方式,并且这些通信方式好像并不相关?
原因在于这些通信方式其实不都是一个人设计出来的,都是不同实验室研发出来的,各有各的优劣势
问题2. 为什么不用void* 接受两个结构体指针呢?而是用struct sockaddr*?
原因在于void*一开始C语言是不支持的,要到后面才支持,而此时通过原来系统接口函数写的代码程序,都不能再进行修改!代码量太大了!
这也是为什么C语言这些用户级语言,更新的时候,不能随意修改原来的程序,而是需要在和以前代码兼容的情况下,再进行更新.
3.2.2 网络字节序
我们知道不同机器在内存中存储数据的方式可能是不同的(以一个字节为单位),分为大端和小端两种字节序.
-
大端字节序
- 指数据的低位存储在高地址,数据的高位存储在内存的低地址
-
小端字节序
- 指数据的低位存储在低地址,数据的高位存储在内存的高地址
下图是16(int 类型),在大端和小端两种不同的存储方式

而在我们之前也写了一段程序验证过,我们当前电脑是以小端字节序进行存储.
链接: 数据在内存中的存储方式
但我们怎么能保证我们进行通信的另一部主机不是以大端字节序进行存储数据的呢?
按照不同字节序进行存储,即便成功发送信息,最后识别出来的结果也还是错误的!
因此,网络通信非常有必要规定统一的网络字节序!
最后确定下来大端作为网络字节序,也就是说假如你主机本身是按照大端存储,则没有任何影响,但假如你是按照小端进行存储,则我们需要在数据发送之前,将小端序转成大端存储的方式!
而在系统接口函数中,也给我们提供了对应转网络字节序的函数

其中h代表host,to 就是发往的意思,n代表Internet,l代表long类型,s代表short类型
htons函数,就是将小端字节序(host byte)转成网络字节序(大端 network byte)
3.3服务器端server编写
3.3.1 初始化服务器
初始化服务器函数,总共有两个任务需要完成
1.创建套接字
我们先来看第一个任务——创建套接字
int socket(int domain, int type, int protocol);
我们需要调用的API函数是socket

它总共有三个参数
第一个参数domain,用来指定协议家族


第二个参数type,用来指定socket的类型


第三个参数protocol,用来指定要与套接字一起使用的特定协议,假如给0在,则使用适用于请求的套接字类型的未指定默认协议
(可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议)

在实际程序编写时,第一个参数我们指定AF_INET(IPV4协议),第二个参数我们指定socket类型选择INET_DGRAM(数据报,无连接,不可靠),第三个参数我们指定0,采取适用于数据报类型的默认协议.
假如创建成功,会返回一个非0整数,文件描述符;否则创建失败,返回-1,并设置对应的errno错误码

//1.创建socket接口,创建对应的网络文件
_sock = socket(AF_INET,SOCK_DGRAM,0);
//假如创建失败,
if(_sock < 0)
{
std::cerr << "create socket err: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _sock << std::endl;
2.绑定
第二个任务是绑定
完成第一个任务(创建套接字)后,我们可以理解为创建了一个网络文件(linux系统下一切皆文件)
在内核层面上就形成了一个对应的struct file结构体,同时该结构体的首地址会被填入到了fd_array数组当中下标为3的位置,即文件描述符(inode)3被占用
对于一般文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作.
而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,因此数据最终就发送到了网络当中.
但这还不足够,在OS看来,我们只是创建了一个文件而已,它还没有真正成为一个网络文件,还没有和网络绑定起来!
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

它总共有三个参数
第一个参数sockfd,也就是我们刚刚获取到的文件描述符

第二个参数address,就是我们的sockaddr_in结构体地址

第三个参数address_len,就是我们的sockaddr_in结构体大小

在实际调用时,我们只需要创建一个sockaddr_in结构体
往里面填充好对应的协议家族,端口号,IP地址
然后再调用bind系统接口函数,即可完成绑定
Warning:
云服务器,或者一款服务器,一般不要指明某一个确定的IP;
本地安装的虚拟机,或者物理机器则是允许的
原因:
一个进程可以关联多个端口号
一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,但假如绑定某一个特定的IP地址,那只能从绑定IP对应的网卡接收数据;
但假如随机绑定(INADDR_ANY),那就能从随机一张对应IP的网卡接收数据,可以提高服务器的负载均衡能力,并避免单个网卡成为瓶颈
//2.绑定
struct sockaddr_in local;
bzero(&local,sizeof(local)); //将结构体清零
local.sin_family = AF_INET; //确定协议家族
local.sin_port = htons(_port); //将端口号转成大端字节序
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip地址切割,然后放进对应套节字位置
//云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; //随机指定任意一个ip
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)))
{
std::cerr << "bind socket err: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
3.3.2 实现start函数
start函数,同样有两个任务需要完成
这就是我们前面提到的,服务器端,可以接受来自用户端的数据,也可以向用户端发送对应的数据
并且收发数据是一个死循环代码,一直while不断执行,为我们用户端提供相应的服务
1.收数据(接受用户端的数据)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

总共有六个参数
第一个参数是socket,就是我们的文件描述符,我们需要从对应的网络文件中取数据

第二个参数是buffer,第三个参数是length
前者是缓冲区的地址,后者是缓冲区的大小,取出来的数据,总应该要有地方存起来吧?所以也很好理解

第四个参数是阻塞还是非阻塞模式的设置

第五个参数是address,第六个参数是address_len
这和我们前面bind函数提到的参数是类似的,只不过此时换成输入输出型参数(传结构进去,函数内部会帮你填充)
前者是我们的sockaddr_in结构体地址,后者是我们的sockaddr_in结构体大小
我们从用户端那收到的,除了数据(存到buffer里)之外,还要有用户端的sockaddr_in信息(包含ip,port,sin_family协议家族等等),有了用户端的sockaddr_in信息,我们才可以向用户端发送对应我们的客户端信息!

成功接收数据,将会成功接收到的字节数,否则返回-1,并设置对应的错误码erno

//创建缓冲区
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //peer结构体的大小,所读缓冲区的大小
//接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0'; //在字符串末尾放一个\0
uint16_t clientport = ntohs(peer.sin_port); //转成小端
std::string clientip = inet_ntoa(peer.sin_addr); //将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else
{
std::cout << "recv err" << std::endl;
}
2.发数据(向用户端发送数据)
发数据和接收数据几乎完全类似
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,用来指定是阻塞写入还是非阻塞写入

第五个参数是dest_addr,第六个参数是 addrlen
其实就是我们刚刚通过recvfrom获取到的用户端sockaddr_in结构体信息,前者是地址,后者是大小

假如成功发送,则返回成功发送的字节数,否则,返回-1,并设置对应的错误码errno

sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
3.3.3 start函数整体代码展示
本质就是一个死循环函数,不断接收来自用户端的请求,并给出对应的服务(响应)
void Start()
{
//服务器一旦启动,就一直死循环的进行下去
while(true)
{
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //peer结构体的大小,所读缓冲区的大小
//接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0'; //在字符串末尾放一个\0
uint16_t clientport = ntohs(peer.sin_port); //转成小端
std::string clientip = inet_ntoa(peer.sin_addr); //将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else
{
std::cout << "recv err" << std::endl;
}
//发 将收到的数据重新发回来
//不需要再加1,发送的时候,不会给你带\0
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
3.3.4 整体代码展示(封装版本)
namespace ns_server
{
const static uint16_t default_port = 8080;
enum{
SOCKET_ERR = 1,
BIND_ERR
};
class udp_Server
{
public:
udp_Server(uint16_t port = default_port)
:_port(port)
{
std::cout << "server addrs: " << _port << std::endl;
}
void InitServer()
{
//1.创建socket接口,创建对应的网络文件
_sock = socket(AF_INET,SOCK_DGRAM,0);
//假如创建失败,
if(_sock < 0)
{
std::cerr << "create socket err: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _sock << std::endl;
2.绑定
struct sockaddr_in local;
bzero(&local,sizeof(local)); //讲结构体清零
local.sin_family = AF_INET; //确定协议家族
local.sin_port = htons(_port); //将端口号转成大端字节序
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip地址切割,然后放进对应套节字位置
//云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; //随机指定任意一个ip
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)))
{
std::cerr << "bind socket err: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
}
void Start()
{
//服务器一旦启动,就一直死循环的进行下去
while(true)
{
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //peer结构体的大小,所读缓冲区的大小
//接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0'; //在字符串末尾放一个\0
uint16_t clientport = ntohs(peer.sin_port); //转成小端
std::string clientip = inet_ntoa(peer.sin_addr); //将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else
{
std::cout << "recv err" << std::endl;
}
//发 将收到的数据重新发回来
//不需要再加1,发送的时候,不会给你带\0
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
~udp_Server()
{}
private:
int _sock;
std::string _ip; //ip地址
uint16_t _port; //端口号
};
}
3.4用户端client编写
3.4.1 初始化用户端
用户端和服务端有什么不同呢?
第一步也是创建socket,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "create socket fail:" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
第二步依旧是将我们的网络文件,正式和网络进行绑定
但是这一部分的代码我们并不需要自己编写,由操作系统OS替我们完成,OS会随机给我们分配一个合适的端口号
于是就有人疑惑了,原来网络文件可以由OS自己自动操作绑定的,那为什么服务器端需要我们自己绑定呢?
原因其实就在于服务器端是公司的服务器不断运行的!
一个公司有多台服务器,每台服务器在公司都会分配特定的端口号,便于统一规范化管理,因此server的端口不能随意改变的
其次,服务器运行起来就不会在停止了,我们将服务器端的端口号绑定有助于有规划的对主机中的端口号进行使用
那为什么用户端又不需要进行端口号的绑定呢?
原因也还是和公司有关,我们手机上肯定有着不同的软件app,一旦运行起来,其实就会绑定对应的端口号,ip,那假如端口号是不同公司自己自行绑定的,那就可能会出现一个问题
当有其他程序也准备使用这个端口号或者说端口号已经被使用,则可能会导致服务器一直启动不起来
比如微信说我要绑定端口号8081,抖音也说我要绑定端口号8081,那端口号就一个,我们前面说过,一个端口号是不可能对应多个进程的!难不成我们打开微信之后,就不能打开抖音了吗?
PS:那服务器什么时候Bind呢?
在我们首次系统调用发送数据的时候,os会在底层随机选择clientport,clientip进行绑定
3.4.2 运行用户端
初始化用户端后,就是运行用户端
向服务器发数据,收数据,都离不开一个问题
如何找到对应服务器端的唯一特定进程?
在编写服务器端的时候,服务器端并不存在这个问题,存在多个用户端访问服务器的情况,服务器端要做的,只是需要在接收的时候(调用recvfrom接口),自己设定一个sockaddr_in结构体,获取对应的用户端ip和port号,两个进程就可以建立起联系,调用sendto接口向对应进程发消息(服务).
但是用户端不同,用户是没有对应服务器端的ip和端口号的!
你不能说调用recvfrom接口,来获取服务器端的ip和端口号,用户是访问者,而不是被访问者
所以用户端进程获取服务器端的ip和端口号,不是用输入输出型参数的方法,而是我们自己给!
在linux系统下,是这样运行的,需要我们自己提供服务器端的ip和端口号,我们点开手机上的app,其实也已经为程序自动提供服务器的端口和ip,以为用户提供服务
./文件名.cpp serverip serverport
在主程序中,我们需要加上对应的判断切割与对应的用户使用手册
//用户使用手册
static void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
//主函数部分代码
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[1]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
u_int16_t serverport = atoi(argv[2]);
...
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());
1.收数据(接收服务器端的数据)
收数据和服务器端也是类似的,同样是调用recvfrom接口
不过有一点不同,用户的ip和port号,是OS操作系统随机指定的,当然对于编写程序并不没有什么差别
//用户输入
std::string message;
std::cout << "Please input your message#";
std::cin >> message;
//发送
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
2.发数据(向服务器端发数据)
发数据和服务器端也是类似的,同样是调用sendto接口
整体思路:用户输入数据,然后调用sendto接口
//用户输入
std::string message;
std::cout << "Please input your message#";
std::cin >> message;
//发送
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
3.4.3 整体代码展示
#include "udp_client.hpp"
//127.0.0.1 本地环回 lo(loop) 进行测试客户端,服务器代码
//./文件.cpp serverip serverport
static void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[1]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
u_int16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
std::cerr << "create socket fail:" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
// 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
// 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
// 需要统一规范化
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());
while(true)
{
//用户输入
std::string message;
std::cout << "Please input your message#";
std::cin >> message;
//什么时候bind?在我们首次系统调用发送数据的时候,os会在底层随机选择clientport,clientip进行绑定
//发送
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)-1,0,(struct sockaddr*)&temp,&len);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
3.5简单应用
3.5.1 echo通讯
//client.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
//client.cc
#include "udp_client.hpp"
//127.0.0.1 本地环回 lo(loop) 进行测试客户端,服务器代码
//./文件.cpp serverip serverport
static void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[1]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
u_int16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
std::cerr << "create socket fail:" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
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());
while(true)
{
//用户输入
std::string message;
std::cout << "Please input your message#";
std::cin >> message;
//什么时候bind?在我们首次系统调用发送数据的时候,os会在底层随机选择clientport,clientip进行绑定
//发送
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)-1,0,(struct sockaddr*)&temp,&len);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
//server.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
namespace ns_server
{
const static uint16_t default_port = 8080;
enum{
SOCKET_ERR = 1,
BIND_ERR
};
class udp_Server
{
public:
udp_Server(uint16_t port = default_port)
:_port(port)
{
std::cout << "server addrs: " << _port << std::endl;
}
void InitServer()
{
//1.创建socket接口,创建对应的网络文件
_sock = socket(AF_INET,SOCK_DGRAM,0);
//假如创建失败,
if(_sock < 0)
{
std::cerr << "create socket err: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _sock << std::endl;
//2.绑定
struct sockaddr_in local;
bzero(&local,sizeof(local)); //讲结构体清零
local.sin_family = AF_INET; //确定协议家族
local.sin_port = htons(_port); //将端口号转成大端字节序
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip地址切割,然后放进对应套节字位置
//云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; //随机指定任意一个ip
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)))
{
std::cerr << "bind socket err: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
}
void Start()
{
//服务器一旦启动,就一直死循环的进行下去
while(true)
{
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //peer结构体的大小,所读缓冲区的大小
//接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0'; //在字符串末尾放一个\0
uint16_t clientport = ntohs(peer.sin_port); //转成小端
std::string clientip = inet_ntoa(peer.sin_addr); //将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else
{
std::cout << "recv err" << std::endl;
}
//发 将收到的数据重新发回来
//不需要再加1,发送的时候,不会给你带\0
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
~udp_Server()
{}
private:
int _sock;
std::string _ip; //ip地址
uint16_t _port; //端口号
};
}
//server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;
//./文件.cpp serverport
static void Usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}
// ./udp_server port
int main(int argc,char* argv[])
{
//假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udp_Server> serv(new udp_Server(port));
serv->InitServer();
serv->start();
return 0;
}
3.5.2 小写字母转大写字母
假如我们在类内部加上新的成员变量,用C++的方式,就是调用functional库,指定输入参数是string,返回参数是string
在创建server对象的时候,添加输入对应的方法即可,而不再需要关注对hpp的修改,只要专注编写对应的服务即可
using func_t = std::function<std::string(std::string)>;

//修改过后的server.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
namespace ns_server
{
const static uint16_t default_port = 8080;
enum{
SOCKET_ERR = 1,
BIND_ERR
};
using func_t = std::function<std::string(std::string)>;
class udp_Server
{
public:
udp_Server(func_t sv,uint16_t port = default_port)
:_service(sv),_port(port)
{
std::cout << "server addrs: " << _port << std::endl;
}
void InitServer()
{
//1.创建socket接口,创建对应的网络文件
_sock = socket(AF_INET,SOCK_DGRAM,0);
//假如创建失败,
if(_sock < 0)
{
std::cerr << "create socket err: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _sock << std::endl;
//2.绑定
struct sockaddr_in local;
bzero(&local,sizeof(local)); //讲结构体清零
local.sin_family = AF_INET; //确定协议家族
local.sin_port = htons(_port); //将端口号转成大端字节序
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip地址切割,然后放进对应套节字位置
//云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; //随机指定任意一个ip
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)))
{
std::cerr << "bind socket err: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
}
void Start()
{
//服务器一旦启动,就一直死循环的进行下去
while(true)
{
//收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //peer结构体的大小,所读缓冲区的大小
//接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0'; //在字符串末尾放一个\0
uint16_t clientport = ntohs(peer.sin_port); //转成小端
std::string clientip = inet_ntoa(peer.sin_addr); //将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else
{
std::cout << "recv err" << std::endl;
}
//业务处理
std::string message = _service(buffer);
//发 将收到的数据重新发回来
//不需要再加1,发送的时候,不会给你带\0
sendto(_sock,message.c_str(),message.size(),0,(struct sockaddr*)&peer,len);
}
}
~udp_Server()
{}
private:
int _sock;
std::string _ip; //ip地址
uint16_t _port; //端口号
func_t _service; //对应处理方法
};
}
//修改后的server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;
static void Usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}
//上层的业务处理
std::string TransformString(std::string request)
{
std::string result;
char c;
for(auto &r: request)
{
if(islower(r))
{
c = toupper(r);
result.push_back(c);
}
else
result.push_back(r);
}
}
// ./udp_server port
int main(int argc,char* argv[])
{
//假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udp_Server> serv(new udp_Server(TransformString,port));
serv->InitServer();
serv->start();
return 0;
}
3.5.3 服务器端处理指令,并将结果返回
我们平时用Xshell,用网络把指令发送到服务器端上,再把结果返回回来,它最基础的底层代码就是这样
我们其实始终都还在Windows下运行我们的用户端程序,只不过服务器端将我们输入的指令结果,经过服务器端处理后,发送返回给我们用户端而已
//修改后的server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;
static void Usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}
//在本地把命令输给服务端,服务端处理后,发回给用户端
//ls -a -l
static bool IsPass(const std::string command)
{
bool pass = true;
auto pos = command.find("rm");
if(pos != std::string::npos) pass = false;
pos = command.find("kill");
if(pos != std::string::npos) pass = false;
pos = command.find("mv");
if(pos != std::string::npos) pass = false;
pos = command.find("while");
if(pos != std::string::npos) pass = false;
return pass;
}
std::string ExcuteCommand(std::string command)
{
//1.安全检查
if(!IsPass(command)) return "you are bad!";
//2.调用popen函数
FILE* fp = popen(command.c_str(),"r");
if(fp == nullptr)
{
std::cerr << "open fail" << strerror(errno) << std::endl;
return "null";
}
//3.获取对应的文件内容
std::string result;
char buffer[1024];
while(fgets(buffer,sizeof(buffer),fp) != nullptr)
{
result += buffer;
}
//4.关闭文件,并返回string
pclose(fp);
return result;
}
// ./udp_server port
int main(int argc,char* argv[])
{
//假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udp_Server> serv(new udp_Server(ExcuteCommand,port));
serv->InitServer();
serv->start();
return 0;
}
3.5.4 多线程改造,实现简易多人聊天室
第一个改造点
服务器有收,有发 可以联想到什么知识?没错,就是我们的多线程!
用户端,服务器端都可以改造为多线程版本
一个线程专门负责发消息,另一个线程专门负责收消息
第二个改造点
假如我们想要实现微信群聊,应该怎么做呢? 一个用户发消息,所有用户都能看到他发的消息 这就需要我们对服务器端进行相应的改造
需要一个容器,将我们所有用户的ip+端口号存储起来 发消息的时候,遍历整个容器,不断调用sendto系统接口同时,消息还要指明是谁发的,因此,服务器端收消息的时候,也需要一个容器(任务队列),将所有用户的ip+端口号+对应它发的消息存储起来
综合两者来看,我们的服务器端需要添加两个新的成员变量,、
1.一个任务队列,用来存储所有用户的ip+端口号+发送的消息;
2.一个聊天室名单,用来存储所有用户的ip+端口号,遍历它向所有用户发消息.
第三个改造点
有多线程,有任务队列
肯定就离不开加锁
第四个改造点
将所有的错误,全部封装到hpp中
整体代码:
锁,任务队列,线程全部采用我们自己封装的代码
//err.hpp
#pragma once
enum{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
//Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>
#include <functional>
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
} ThreadStatus;
// typedef void (*func_t)(void *);
using func_t = std::function<void ()>;
public:
Thread(int num, func_t func) : _tid(0), _status(NEW), _func(func)
{
char name[128];
snprintf(name, sizeof(name), "thread-%d", num);
_name = name;
}
int status() { return _status; }
std::string threadname() { return _name; }
pthread_t threadid()
{
if (_status == RUNNING)
return _tid;
else
{
return 0;
}
}
// 是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
// 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
static void *runHelper(void *args)
{
Thread *ts = (Thread*)args; // 就拿到了当前对象
// _func(_args);
(*ts)();
return nullptr;
}
void operator ()() //仿函数
{
if(_func != nullptr) _func();
}
void run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this);
if(n != 0) exit(1);
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if( n != 0)
{
std::cerr << "main thread join thread " << _name << " error" << std::endl;
return;
}
_status = EXITED;
}
~Thread()
{
}
private:
pthread_t _tid;
std::string _name;
func_t _func; // 线程未来要执行的回调
ThreadStatus _status;
};
//RingQueue.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
static const int N = 256;
template <class T>
class RingQueue
{
public:
void P(sem_t&m)
{
sem_wait(&m);
}
void V(sem_t&m)
{
sem_post(&m);
}
void Lock(pthread_mutex_t& l)
{
pthread_mutex_lock(&l);
}
void Unlock(pthread_mutex_t& l)
{
pthread_mutex_unlock(&l);
}
public:
RingQueue(int num = N):_ring(num),_cap(5)
{
sem_init(&_data_sem,0,0); //数据信号量初值为0
sem_init(&_room_sem,0,num); //空间信号量初值为num
_c_step = _p_step = 0;
pthread_mutex_init(&_c_mutex,nullptr);
pthread_mutex_init(&_p_mutex,nullptr);
}
~RingQueue()
{
sem_destroy(&_data_sem);
sem_destroy(&_room_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
//生产
void push(const T&in)
{
P(_room_sem); //申请空间信号量
Lock(_p_mutex); //加锁,实现多生产者互斥关系
_ring[_p_step++] = in;
_p_step %= _cap; //判断是否越界,及时返回
Unlock(_p_mutex);
V(_data_sem); //数据信号量加1
}
//消费
void pop(T *out)
{
P(_data_sem); //申请数据信号量
Lock(_c_mutex);
*out = _ring[_c_step++];
_c_step %= _cap;
Unlock(_c_mutex);
V(_room_sem); //空间信号量加1
}
private:
std::vector<T> _ring; //循环队列
int _cap; //队列容量
int _c_step; //消费者生产的位置
int _p_step; //生产者消费的位置
sem_t _data_sem; //数据资源,只有消费者关心
sem_t _room_sem; //空间资源,只有生产者关心
pthread_mutex_t _c_mutex; //消费者锁
pthread_mutex_t _p_mutex; //生产者锁
};
//Mymutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* mutex):pmutex(mutex)
{}
~Mutex()
{}
void Lock()
{
pthread_mutex_lock(pmutex);
}
void Unlock()
{
pthread_mutex_unlock(pmutex);
}
private:
pthread_mutex_t* pmutex;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex):_mutex(mutex)
{
//在创建的时候,就自动上锁
_mutex.Lock();
}
~LockGuard()
{
//销毁的时候,自动解锁
_mutex.Unlock();
}
private:
Mutex _mutex;
};
其中服务器端server进行了封装
//server.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unordered_map>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "RingQueue.hpp"
#include "mymutex.hpp"
#include "err.hpp"
#include "Thread.hpp"
namespace ns_server
{
const static uint16_t default_port = 8080;
using func_t = std::function<std::string(std::string)>;
class udp_Server
{
public:
udp_Server(uint16_t port = default_port)
: _port(port)
{
std::cout << "server addrs: " << _port << std::endl;
pthread_mutex_init(&_lock, nullptr); // 锁初始化
_p = new Thread(1, std::bind(&udp_Server::Recv, this));
_c = new Thread(1, std::bind(&udp_Server::Broadcast, this));
}
void start()
{
// 1.创建socket接口,创建对应的网络文件
_sock = socket(AF_INET, SOCK_DGRAM, 0);
// 假如创建失败,
if (_sock < 0)
{
std::cerr << "create socket err: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _sock << std::endl;
// 2.绑定
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 将结构体清零
local.sin_family = AF_INET; // 确定协议家族
local.sin_port = htons(_port); // 将端口号转成大端字节序
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip地址切割,然后放进对应套节字位置
// 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
if (bind(_sock, (struct sockaddr *)&local, sizeof(local)))
{
std::cerr << "bind socket err: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
_c->run();
_p->run();
}
void AddUser(const std::string &name, const struct sockaddr_in &peer)
{
LockGuard lockguard(&_lock); // 加锁
auto iter = onlineuser.find(name);
if (iter != onlineuser.end())
return; // 假如已经有对应键值对,则直接返回
onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
}
void Recv()
{
// 服务器一旦启动,就一直死循环的进行下去
while (true)
{
// 收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // peer结构体的大小,所读缓冲区的大小
// 接受的c字符串,默认最后一个是以\0结尾,所以有效消息长度要减去1
int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = '\0'; // 在字符串末尾放一个\0
}
uint16_t clientport = ntohs(peer.sin_port); // 转成小端
std::string clientip = inet_ntoa(peer.sin_addr); // 将ip(整型)转成字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
// 构建一个用户,并检查
std::string user = clientip;
user += ":";
user += std::to_string(clientport);
AddUser(user, peer);
//rq.push(buffer);
//假如我们还想要知道是谁发的消息
std::string message = user + ": " + buffer;
rq.push(message);
}
}
void Broadcast()
{
while (true)
{
std::string sendString;
rq.pop(&sendString);
// LockGuard lockguard(&_lock); //加锁
// for (auto user : onlineuser)
// {
// std::cout << "Broadcase message to " << user.first << sendString << std::endl;
// sendto(_sock, sendString.c_str(), sendString.size(), 0, (struct sockaddr *)&(user.second), sizeof(user.second));
// }
std::vector<struct sockaddr_in> v;
{
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));
}
}
}
~udp_Server()
{
pthread_mutex_destroy(&_lock);
_c->join();
_p->join();
delete _p;
delete _c;
}
private:
int _sock;
std::string _ip; // ip地址
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;
};
}
//server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;
static void Usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}
// ./udp_server port
int main(int argc,char* argv[])
{
//假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udp_Server> serv(new udp_Server(port));
serv->start();
return 0;
}
用户端client则没有进行封装
//client.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
//client.cc
#include "udp_client.hpp"
// 127.0.0.1 本地环回 lo(loop) 进行测试客户端,服务器代码
static void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
void *recv(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';
std::cout << "server echo# " << buffer << std::endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[1]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
u_int16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "create socket fail:" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
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());
//创建多线程
pthread_t tid;
pthread_create(&tid, nullptr, recv, &sock);
while (true)
{
std::string message;
std::cout << "Please input your message#";
std::getline(std::cin, message);
// 发送
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
return 0;
}

1119

被折叠的 条评论
为什么被折叠?



