背景
1.编程实现PING的服务器端和客户端,实现操作系统提供的ping命令的类似功能。
2.服务器端PingServer功能:
2.1 可以并发地为多个用户服务;
2.2 显示用户通过客户端发送来的消息内容(包含头部和payload);
2.3 能够模拟分组的丢失;能够模拟分组传输延迟;
2.4 将用户发送来的请求request在延迟一段随机选择的时间(小于1s)后返回给客户端,作为收到请求的响应reply;
2.5 通过如下命令行启动服务器:java PingServer port。port为PingServer的工作端口号
3.客户端PingClient功能:
3.1启动后发送10个request。发送一个request后,最多等待1秒以便接收PingServer返回的reply消息。如果在该时间内没有收到服务器的reply,则认为该请求或对该请求的reply已经丢失;在收到reply后立即发送下一个request。
3.2请求消息的payload中至少包含关键字PingUDP、序号、时间戳等内容。如:PingUDP SequenceNumber TimeStamp CRLF
其中:CRLF表示回车换行符(0X0D0A);TimeStamp为发送该消息的机器时间。
3.3 为每个请求计算折返时间(RTT),统计10个请求的平均RTT、最大/小RTT。
3.4 通过如下命令行启动:java PingClient host port。host为PingServer所在的主机地址;port为PingServer的工作端口号
完成后的源码
服务器端代码
|
|
客户端代码
|
|
实现过程的难点、疑惑以及思考
下面是我遇到的一些问题以及解决方案,还有自己的一些思考
不清楚报文格式
不清楚ICMP头部以及payload的格式,甚至连payload都不知道指的是什么。。然后查了别人的博客才清楚,也可以参考RFC的文档RFC792
客户端控制最多等待1秒
题目有要求客户端最多对每个ping请求报文等待1秒,然而,BIO为阻塞IO,并不能主动控制阻塞时间。即如果用BIO实现,则需要等到收到应答才能判断是否超时(判断是否大于1秒),假如服务器一直没有回应,将会让程序一直在接收语句处阻塞。为了解决这一问题,可以使用NIO(Non-Blocking IO)来解决。NIO为非阻塞IO,可以在非阻塞模式下接收数据报,因此程序可以加入计时功能的代码,在达到一秒后主动结束等待。关于NIO的知识可以见我之前写的NIO的系列文章
Java实现校验和的计算
校验和算法我是了解的,网上也能够找到c语言的实现,然而就是觉得写起来很烦,毕竟要进行位操作,所以就到StackOverflow上面借(chao)鉴(xi)了别人的实现,详情见StackOverflow
对真正Ping程序的实现以及数据报在整个过程的传输过程感到疑惑
这是令我最头大的一个问题,可以说我对Ping程序运行过程的数据报传输经过是一头雾水。为什么?因为我现在使用的是UDP来模拟用ICMP协议进行通信,但是ICMP可是网络层的协议啊,也就是说Ping的真正实现中是不需要用到传输层的UDP和TCP协议的,那么应该如何实现?由于这个时候我对Socket的理解仅停留在只有UDP Socket和TCP Socket的层面,所以这个问题我纠结了很久。
查了一些资料之后,我发现有人用raw Socket来实现Ping,于是开始去了解raw Socket的有关内容。可以参考《Linux网络编程:原始套接字的魔力【上】》和《有关raw socket的一些知识》这两篇文章,讲的很好。
但是看完这些,我还是对真正的Ping的核心实现一知半解,数据报传递的过程究竟是怎样的呢?幸运的是我看到了一篇非常好的文章,《linux内核网络分层结构》,尤其是文中的下面这幅图,将硬件、计网、操作系统的知识融会贯通了之后再看会有恍然大悟的感觉(不过图中没有标识网络层)
另外,由于我对一些硬件知识还是不清楚,尤其是对设备控制器、驱动程序等概念一知半解,然后找到了这篇博文设备、设备控制器和驱动程序,里面讲的比较清楚。
最后,我决定去看一下Ping的源码,验证一下是否用的raw Socket,然后发现居然不是,而是用的icmp Socket,详情见ping源码分析 。不过这时候我的思路已经没有之前那么局限了,所以自己思考一下还是可以理解采用这种socket后,在实现中应该做的事,以及数据报在Ping的运行过程中的传输过程。我的理解是,在ip_local_deliver_finish()函数处理后,检查IP报头的协议字段,然后将报文正文传递给icmp的接收函数,接着再将数据传输到使用了icmp Socket的Ping应用,到达了用户空间。