(两个月以前,我参加了某个比赛。做了个跟socket服务器有关的项目。比赛完后本来就想立刻写个博文总结一下我的技术经验的,结果拖延症晚期一从紧张的环境中解放就开始各种浪……直到现在才有点心情敲文字)
(我做的东西很简单。跟微信端,网页前端,硬件开发板,后台服务器都有关。我负责的是硬件开发板和后台服务器的代码部分,我尝试着都稍微总结一下,贴点代码吧……)
(先从后台服务器开始吧,这点的内容比较少也比较有用)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
怎么使用java实现socket通信,这个用关键词一搜,我想网上应该就有一大堆现成的代码了吧,所以这些基础的基础我就不讲了。我以问题作为标题的形式总结一下我在实现服务器过程中磨蹭出来的几个小经验。
(对了,我使用的是阿里云10块钱一个月的学生服务器,操作环境是windows server 2008,搭了java、Apache、MySQL、PHP的环境。)
(推荐一个调试socket时很好用的工具:SocketTool 。免安装,只有一个exe文件,轻便简洁。使用方法打开就知道了。)
①小经验:服务器在与客户端建立连接之后,CPU使用率总是100%怎么办?
原因:建立连接后启动新的线程,如果线程中有简单粗暴的不含阻塞的while(1)循环,会持续占用CPU,导致CPU占用率极高。
解决:在while(1)的大循环中插入一句sleep(1),即阻塞1毫秒,java线程内则使用Thread.sleep()的静态方法阻塞线程。效果奇佳如图(用C语言演示)
图:优化前(我的电脑是四核cpu,所以单线程无限无阻塞循环占用率不会达到100%)
图:优化后
分析:这个问题几乎有可能出现在所有持续运作的程序上。虽然只是阻塞了程序一毫秒,但是如果循环内的运算量不大的话,阻塞一毫秒几乎让出了整个CPU的运算资源,还不影响程序的运行。理想情况下,假设原本执行一次循环只需要消耗10个CPU周期的话,如果不进行阻塞,2Ghz的CPU在一秒内会执行2*10^9/10=2*10^8次的循环,然而在1秒内执行那么多次循环对我们的程序一点帮助都没有,还会抢占CPU资源;而阻塞该程序1ms后,相当于每进行一次循环后就让出1ms的运算资源,也就是让出2*10^6个cpu周期,原本占用100%的程序只会占用不到1万次CPU周期,这对于2Ghz的CPU来说几乎是0负担的。以前我混过ACM竞赛,往往敲的代码都是追求指定的时间内完成大规模数据的运算,所以,在while循环中是绝对不可能出现sleep()这种拖时间的代码的,然而,程序在实际应用中,一个小小的sleep()还是必不可少的。
②小知识:为什么socket客户端在非正常断开后,服务器在很长一段时间内不会显示客户端断开?(或:为什么保持长连接需要“心跳包”?)
原因:如果是正常终止客户端进程,系统会对进程正在占用的资源进行回收。此时客户端所占用的socket端口会被释放,服务器端也会被告知对方断开socket连接了,因此终止与该客户端的连接。而如果客户端非正常断开(比如客户端主机突然断电),则客户端的操作系统会在第一时间产生中断,保护操作系统。哪个操作系统还会特意耗费时间去回收socket资源←_←。所以即便客户端明明已经断开了连接,但服务器却迟迟没办法知道客户端断开的消息。因此会在较长一段时间内不会告诉编程者“某客户端已断开”,即便你写了在客户端断开后立刻回收资源的代码。
解决:相信不少人都听说过“心跳包”吧。保持客户端与服务器长连接的话,心跳包是必不可少的。设定阈值n,心跳间隔时间T,当服务器在n*T的时间内没有听到客户端的心跳,那么就可以判定客户端“死亡”了,主动与它断开连接,回收资源。至于n和T怎么设置,纯看项目的需求和编程者的心情了。当然,心跳包的写法很多种,可以是服务器向客户端发心跳要求回应,也可以是客户端直接向服务器发心跳。不同的应用场景可能都不同。
血的经历:当网络延迟高于n*T的时候,唉……(曾经设置n=2,T=8s,然而网络延迟——20000ms……别问我这种奇迹怎么发生,你在局域网边上开上百个路由器就知道了)
③小知识:什么是阻塞式IO,什么是非阻塞式IO?区分他们有何用?
阻塞式IO:IO即input/output,阻塞式IO指的是“一旦输入/输出工作没有完成,则程序阻塞,直到输入/输出工作完成”。在目前,我们从书本上学到的语法用的基本都是阻塞式IO。比如c语言的stdio.h库的所有函数(包含scanf(),getchar(),gets()等函数),Java的BIO(比如各类输入输出流)。他们都是不见黄河心不死的好汉。在你满足他们的条件之前,不让你的程序继续往下跑。最简单的例子:c语言的scanf()函数——当你scanf()要求输入两个数字时,你只输入一个数字,它也不会让你继续执行接下来的代码的。
非阻塞式IO:JAVA有两种非阻塞式IO——AIO和NIO。说真的,我自己都没去看他们具体是怎么用的……所以我就不阐述了。
有啥用:如果你的socket在n*T的时间内没有读到心跳,然而你读取socket传来的数据,调用的输入函数是阻塞式IO(如下),那么即便没有心跳了,你的程序仍然会被阻塞在该输入函数下无法继续(除非收到数据,然而,这又与没有心跳相矛盾……),这让心跳包显得鸡肋。
以下是阻塞式IO例子代码:
/*常见的阻塞式IO代码*/
serverSocket = new ServerSocket(PORT);
Socket client = serverSocket.accept();
BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream()));
while(1)
{
String reciver=in.readLine();//当输入流没有换行时,在此阻塞
Thread.sleep(1);
}
④小经验:怎么使用java的阻塞式IO实现非阻塞式IO?
实现:相信学过操作系统原理的人都听说过生产者消费者问题吧。我也称这种实现方式为生产者消费者方式,然而目的和内容大相径庭。操作系统中的生产者消费者问题主要解决的问题是资源共享问题。而我解决的是供需问题。实现步骤如下——①写一个生产者类作为消费者的子线程:使用队列存放每一次读到的数据,不论是心跳还是目标数据都塞入队列。当队列不为空的时候,表示该socket口仍在接受数据,否则已经断开。②父线程(也就是消费者)不断向生产者索取数据,如果生产者在n*T的时间间隔内不断地返回“队列为空”的状态,则断开该socket,终止该消费者线程。
原理:通过父线程终止,强迫子线程(生产者类)抛出异常,从而终止输入输出流的持续阻塞状态。
以下是生产者类的代码:
/*生产者类*/
class dataProducer implements Runnable
{
BufferedReader in=null;
Queue inputQueue=new LinkedList();
public dataProducer(InputStream _inputer)
{
in=new BufferedReader(new InputStreamReader(_inputer));
}
public boolean isActive() //用于父线程判断生产者是否有数据可读
{
if(inputQueue.isEmpty())return false;
else return true;
}
public String next() //获得队列的下一个数据
{
return inputQueue.remove();
}
public void run()
{
try
{
while(true)
{
String reciver=in.readLine();
inputQueue.add(reciver);
Thread.sleep(10);
}
}
catch(IOException | InterruptedException e) //被强迫抛出异常后,该线程会执行以下代码
{
System.out.println("Forced interrupted the readline() successfully.");
e.printStackTrace();
}
finally
{
try
{
if(in!=null)in.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
以下是父线程调用生产者类的代码:
long lasstime=System.currentTimeMillis();
long interval=0;
try
{
dataProducer rdl=new dataProducer(client.getInputStream());
Thread thrdl=new Thread(rdl);
thrdl.start();
while(interval<16000)
{
interval=System.currentTimeMillis()-lasttime;
if(!rdl.isActive()){Thread.sleep(10);continue;}
String reciver=rdl.next();
if(reciver 是数据) //判断姿势取决于你的心跳包和数据包怎么设计
{
//处理数据;
}
lasttime=System.currentTimeMillis(); //更新lasttime
Thread.sleep(1);
}
}
catch(Exception e)
{
//异常处理
}
finally
{
//资源回收
}
④小经验:怎么使用c语言实现非阻塞式socket线程?
实现:C语言使用socket实现长连接的话,可以通过setsockopt()来设置recv()的超时,自动跳出阻塞重新进入循环。能够设置超时的话,将超时时间设成n*T,再加个判断就行了。需要注意的是,设置recv()超时的同时,accpet()阻塞也会超时。没力气贴代码了,感觉能够设置超时还是挺方便的。
⑤小经验:服务器怎么在数据库改动后,主动向在线客户端发送数据?
实现:使用全局变量数组(如果是java,则可以选择定义一个静态类存放全局变量)存放客户端的socket,再开一个线程,专门用于广播数据。至于判断数据库改动,我的方法是在数据库中设置dirty_bit(脏位),每次读取数据之后立刻将脏位改为0,然后广播数据。(我实现这个功能的方法很土甚至在数据连续快速改动的时候可能会出现bug,所以不好意思贴代码,我相信会有更好的解决办法的)
(就这样吧。以上内容都是个人经验的总结。没有学术权威的-。- 有问题还请大胆指教)
(日后我会尽力推出跟硬件编程有关的经验分享或者跟最近正在学的python爬虫相关的经验分享)