asio基础使用方法

DinS          Written on 2017/10/28

本文开始正式介绍asio使用方法,希望读者已经把环境搭建好了。

一、基本概念

使用Asio可以写出非常简洁优秀的代码,但是开始写代码之前要先了解几个概念(类)。
第一个:socket。这个应该没有什么疑问,把socket抽象为一个类,类提供了连接、收发信息的成员函数。
第二个:endpoint。在Asio中不管是客户端还是服务端,其地址结构都被抽象为终端(endpoint),这个类中记录了网络通信必要的设置信息,比如ip地址、端口等,至于网络字节序转换等问题程序员就不需要考虑了。
通过这两个概念,可以直接从语义上写出socket.connect(endpoint),非常直观。
第三个:acceptor。这个类仅用于服务端,相当于监听socket。我们知道监听socket每次accpet成功都会返回一个连接socket。连接socket抽象为socket类,而监听socket抽象为acceptor。这样区分开来不容易搞混。
第四个:buffer。这个代表缓存区,是对常用的char[]或者vector[]之类的高级抽象,Asio中全部使用buffer来传递信息。

这四个是最基础的概念,有了这些类就可以写出网络通信代码了。

二、TCP

下面是服务端代码:

亮点颇多.
最大的亮点应该是代码异常简洁,很短,没有准备工作和收尾工作,而且建立连接一共只有4句代码。这个已经是我们能够期望的最好结果了。
其次代码逻辑也很好理解,其思路跟socketAPI是一致的,建立监听socket监听端口,然后等待客户端连接,只不过这里监听socket变成了acceptor类,端口成为了终端endpoint。更加简洁。
最后收发信息也十分顺畅,使用buffer统一做抽象。不过注意收信息只能是char[],用string不行。
其实错误处理也是很方便的,但是这里先略去,稍后介绍。
补充说明:read/write函数与socket成员函数receive/send区别。前者是Asio统一的读写buffer通用函数,而且可以做到recv或者send字节数少于要收发内容时可以自动继续操作,保证收发字节与设定的一致。因此可以说比成员函数更可靠。
这里之所以没用read/write只是想展示一个成员函数的用法。

再来看客户端代码:

依然非常简洁,我们定义好一个终端,建立socket然后connect,就行了.
看一下运行结果。

打开服务端和客户端:

客户端输入内容后显示结果:

三、异常处理

刚刚只是展示了基本使用方法,但是socket必须要处理异常,不然程序不稳定。
Asio提供了非常好的异常处理机制,也就是说不用像windows那样每一步都判定返回值然后处理,而是可以直接使用c++的异常处理机制来做。对c++异常机制不熟悉的看这里《c++异常机制》。
下面是客户端代码,服务端如法炮制。

仅仅是在正常代码外面套了一层try-catch,这样就可以完成异常捕获了。

试一下,编译后直接打开客户端不打开服务端,如下界面:

居然是贴心的自然语言而不是冰冷的错误代码。Nice job!

四、UDP

TCP的使用已经相当简洁了,那么UDP会如何呢?
下面来看服务器代码:

逻辑跟socketAPI一致,建立完socket直接收发信息,不过代码并没有比TCP简单太多。没有用到acceptor,不过需要额外保存客户端终端对象。注意建立tcp还是udp在asio中通过命名空间的方式区别。

再来看看客户端:

大图点这里

代码量跟TCP差不多。
这里有点费解的可能是建立socket的第二个参数为0.这个参数表示端口号。
按理说客户端不需要显示指定自己的端口号,只要有服务端的终端即可,这里的0可以理解为让程序自动选择合适的。

运行结果:

五、小结

使用Asio提供的socket类、endpoint类和acceptor类可以非常简洁、高效地实现网络通信基础功能,看到了这些相信你一定会选择Asio而不是直接使用socketAPI。
除了基础用法,Asio还提供了一些拉风的扩展用法。

六、socket扩展使用方法

1.resolver和query
我们之前直接使用了endpoint来定义终端,实际上还有别的办法,那就是域名解析。关于域名的简短介绍可以看这里http://jingyan.baidu.com/article/2c8c281df0afd00008252aa7.html.
如果要一句话概括,可以把域名理解为ip地址的别名。
ip地址对于人类大脑很难记住,所以给ip起个名字,方便记忆,这就是域名
这里在这里找到域名记录。

用文本编辑工具打开hosts文件,里面有说明,通常会有这么一项。

这个意思就是localhost对应ip地址为127.0.0.1。

有了这个以后看客户端代码:

之前我们直接定义endpoint,这里使用域名解析来得到endpoint。
稍微复杂一点,首先建立一个resolver对象,然后准备一个query对象,构造函数中第一个参数是域名,第二个表示端口。然后对其解析,得到endpoint迭代器,使用这个来connect。
不放心的话使用endpoint.address().to_string()就能够返回string格式的ip地址,endpoint.port()返回端口。可以检查。

为什么返回迭代器呢?因为可选的endpoint不一定只有一个,所以需要程序一个一个尝试,直到找到可以连接的终端。
可以使用asio::connect(socket, resolver.resolve(query));这句代码让程序自动完成这个过程。

2.socket iostream
这个东西非常神奇,可以像cin/cout那样使用socket而不用管具体的连接问题,而且因为是流所以发送和接收的规模也不用管。
这么神奇的东西到底是怎样使用的呢?实际上在《asio环境搭建》里跟python对比的代码就是用流来做的,看代码:

connect也可以直接输入ip地址,效果一样,比如stream.connect(“127.0.0.1”, “6123”);

已经简洁到不能再简洁了。在抽象概念上socket需要的就是对方ip地址和端口号,我们只给了这两个信息,然后就可以像iostream那样进行网络通信了。
先来试一下失败情况,只打开客户端:

再试试正常情况:

成功!使用Asio提供的iostream做网络编程简直是惊天地泣鬼神。妈妈再也不用担心我不会socket编程了。

不过提取出文本还需要一定步骤,但是很简单:

对于提取指定长度的情况,可以这样用字符数组接收。

对于提取全部内容的,可以使用getline(stream, string, char(EOF))。没错可以直接用string,不用担心长度问题。可见跟python对比的代码示例。

另外这个流还有一些其他的方便的功能,比如设定连接时限,用stream.expires_from_now(chrono::seconds(60));之类的。具体可以看文档。
客户端使用这个最合适,服务器由于要处理更复杂情况,还是需要Socket类。

目前展示的只是最简单的一对一模式,Asio能够处理服务器设计吗?当然可以,见《asio服务器模式:多线程或轮询》和《asio服务器模式:异步调用》。