为什么要使用异步事件处理:
在web或者其他应用中,有一些并不是迫切需要返回值的操作,比如发短信,发邮件,下载第三方图片等,
如果这些第三方网络请求都在一次http请求中实现(传统方法使用php执行curl)将会造成客户端等待返回时间较长,
如果遇到第三方服务器出问题,或者dns服务器响应慢等网络问题,可能造成客户端主动断开连接
实际上队列服务有相应的软件,本文只做为原理和编程思想的学习
如果小型的需求也可以直接使用改程序,不过需要考虑单一线程处理效率较低,如果开多个线程,需要考虑线程管理(如线程池)的问题
场景描述:
客户端请求服务器 localhost,服务器需要请求第三方 http://a.360lt.com/ ,
thrift:
是一个跨语言开发框架,支持c、java、php、python等主流语言
使用一个表述声明语言定义接口,通过实现对应接口成员函数实现不用语言之间的调用
1.客户端浏览器请求服务器
2.服务器发送一个事件到Queue服务(通过调用Thrift生成的函数接口),并返回浏览器提示稍后查询
3.Queue收到事件,执行对应程序(如访问第三方服务器拉取数据)
4.Queue将结果保存到数据库
THRIFT 接口声明
service ThriftTest
{
bool sendMsg(1:i64 userId) ,
bool requestRemoteApi(1:string url)
}
JAVA QUEUE服务进程
ThriftTestServer.java
public class ThriftTestServer {
public static void main(String[] args) {
try {
// 设置服务端口为 7911
//TServerTransport serverTransport = new TServerSocket(7911);
//serverTransport.
TServerTransport serverTransport = new TServerSocket(new InetSocketAddress("127.0.0.1", 7911));
// 设置协议工厂为 TBinaryProtocol.Factory
//Factory proFactory = new TBinaryProtocol.Factory();
// 关联处理器与 Hello 服务的实现
TProcessor processor = new ThriftTest.Processor(new ThriftTestImpl());
//TServer server = new TThreadPoolServer(processor, serverTransport, proFactory);
TServer server = new TSimpleServer(new Args(serverTransport).processor(processor));
System.out.println("Start server on port 7911...");
(new Thread(new QueueThread()) ).start(); // start queue thread
server.serve();
} catch (TTransportException e) {
e.printStackTrace();
}
}
}
ThriftTestImpl.java
public class ThriftTestImpl implements ThriftTest.Iface {
@Override
public boolean sendMsg(long userId) throws TException {
// TODO Auto-generated method stub
System.out.println("hello world");
return false;
}
@Override
public boolean requestRemoteApi(String url) throws TException {
// TODO Auto-generated method stub
QueueThread.addUrl(url);
return false;
}
}
QueueThread.java
public class QueueThread implements Runnable {
public static ArrayDeque urls ;
{
QueueThread.urls = new ArrayDeque() ;
}
public static void addUrl(String _url) {
QueueThread.urls.add(_url) ;
}
@Test
public void unitTest(){
try {
doRequest("http://a.360lt.com/");
//saveData("{"title":"a.360 response","content":"response at 2016-04-28 14:14:48"}");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private boolean doRequest(String url){
System.out.println("try to connect " + url);
try {
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection() ;
BufferedInputStream bis = new BufferedInputStream( conn.getInputStream() );
int len = 1024 ;
int off = 0 ;
byte b[] = new byte[len] ;
int readLen = 0;
int realReadLen = 0 ;
ByteArrayOutputStream contentBytes = new ByteArrayOutputStream();
while((realReadLen = bis.read(b, off, len)) != -1) {
contentBytes.write(b);
contentBytes.flush();
readLen += realReadLen ;
}
String jsonStr = new String(contentBytes.toByteArray(), 0, readLen, "utf-8") ;
System.out.println(jsonStr);
saveData(jsonStr) ;
} catch (Exception e) {
//这里应该记录日志
System.out.println("catch " + e.getClass().toString() + " msg is " + e.getMessage());
}
return true;
}
private boolean saveData(String json){
JSONTokener jsonParser = new JSONTokener(json);
JSONObject jsonObj = (JSONObject)jsonParser.nextValue() ;
String title = jsonObj.getString("title") ;
String content = jsonObj.getString("content") ;
Connection gdbcConn = null ;
PreparedStatement ps = null ;
ResultSet res = null ;
try{
Class.forName("com.mysql.jdbc.Driver");
String gdbcUrl = "jdbc:mysql://" +db.HOST+ ":" +db.PORT+ "/" +db.DBNAME+ "?characterEncoding=" +db.CHARSET;
String user = db.USER;
String pwd = db.PWD;
gdbcConn = DriverManager.getConnection(gdbcUrl, user, pwd);
String sql = "insert into demo(title,content,sort) values(?,?,?)";
ps = gdbcConn.prepareStatement(sql);
ps.setString(1, title);
ps.setString(2, content);
ps.setInt(3, 1);
boolean isok = ps.execute();
System.out.println("save data ok");
}catch(Exception e){
System.out.println("catch " + e.getClass().toString() + " msg is " + e.getMessage());
System.out.println(e.getLocalizedMessage());
}finally{
try{
if(ps != null)
ps.close();
if(res != null)
res.close();
if(gdbcConn != null)
gdbcConn.close();
}catch(Exception e){
//记录日志
System.out.println("catch " + e.getClass().toString() + " msg is " + e.getMessage());
}
}
return true;
}
@Override
public void run() {
try{
while(true){
//int cs = QueueThread.urls.size();
while(!QueueThread.urls.isEmpty()){
String _url = QueueThread.urls.pop() ;
doRequest(_url);
}
Thread.sleep(1000);
}
}catch(NoSuchElementException e){
//这个异常应该单独处理,或重启队列服务
System.out.println("catch " + e.getClass().toString() + " msg is " + e.getMessage());
}catch (Exception e) {
//这里应该记录日志
System.out.println("catch " + e.getClass().toString() + " msg is " + e.getMessage());
}
}
}
主要处理程序为 QueueThread 这个类,该类是队列进程的一个子线程
流程:
web端通过requestRemoteApi这个接口不断地向QueueThread中的静态变量urls添加新条目(入队)
QueueThread线程则不断地从中获取(出队)
然后调用doRequest程序,请求第三方接口
最后调用saveData程序,将返回的json格式数据解析并保存到数据库
注:
1.
这里使用队列有可能会由于重启进程造成事件丢失,这里提供一个思路,用Redis来处理
2.
关于处理多个事件,可以定义若干协议,格式如:
{“type”:“sendmsg”,“values”:{“par1”:“val1” …}}
然后制作一个事件处理器,通过不同的事件执行不同的操作,如发短信、发邮件、下载图片、上传图片、同步数据等
3.
如果需要做的灵活些不断地添加事件,可以使用java的反射机制做,封装一个事件处理类,每个成员函数为处理一个事件,协议中包含处理事件的函数名
WEB 服务
index.php
namespace demo ;
require_once 'Thrift/ClassLoader/ThriftClassLoader.php';
use ThriftClassLoaderThriftClassLoader;
use ThriftProtocolTBinaryProtocol;
use ThriftTransportTSocket;
use ThriftTransportTHttpClient;
use ThriftTransportTBufferedTransport;
use ThriftExceptionTException;
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__);
$loader->register();
include 'Types.php' ;
include 'ThriftTest.php' ;
$url = 'http://a.360lt.com/' ;
try{
$socket = new TSocket('127.0.0.1', 7911);
$transport = new TBufferedTransport($socket, 1024, 1024);
$protocol = new TBinaryProtocol($transport);
$cli = new ThriftTestClient($protocol);
$transport->open();
$isok = $cli->requestRemoteApi($url);
$transport->close();
}catch (Exception $e){
echo "catch exception " . get_class($e) . ", message is " . $e->getMessage() ;
}
?>
注:
由于是异步请求,所以并不适合需要立刻返回给客户端的操作(比如获取用户个人信息)
使用的场景一般是发短信、发邮件、发推送、同步其他服务器数据等
特别是需要一次发送多条(需要多次建立连接,发送http请求)的操作,必须上队列服务,否则客户端会由于等待时间过长造成断开连接
index.php
date_default_timezone_set('Asia/ChongQing');
// 模拟一个接口提供端,假设提供端效率较低,这里延迟10m
sleep(3);
$data = array(
'title' => 'a.360 response' ,
'content' => 'response at '.date('Y-m-d H:i:s')
) ;
echo json_encode($data) ;
exit();
?>