首先来看以下我们的需求:
用java编写一个监听程序,监听指定的端口,通过浏览器如http://localhost:7777来访问时,可以把请求到的内容记录下来,记录可以存文件,sqlit,mysql数据库,然后把接受到的信息在浏览器中显示出来
要点:
Socket,线程,数据库,IO操作,观察者模式
来看下我们如何来设计这个小系统,这个系统包含三部分的内容,一个是监听端口,二是记录日志,三是数据回显,端口监听第一想到的就是Socket编程了,数据回显也是一样的,无非是把当前请求客户端的socket获取到,然后把消息通过流输出出去,日志的记录因为是要多种实现策略,这里我们使用了一个观察者模式来实现,服务器可以添加任意多个观察着,因此有着很灵活的扩展性,在实例程序中我们分别提供了ConsoleRecordHandler--直接把获取到的信息打印到控制台,和存放数据库的方式-MysqlRecordHandler,当然你也可以分别提供基于文件的实现。
首先来看我们系统的类图
HttpServer类是我们的核心类,他实现了Runnable接口,因此有着更高的性能,在循环中不断的去轮询指定端口,构造方法比较简单,只需要一个要监听的端口号即可,还有两个用于触发监听和停止程序运行的方法stop()&start(),这两个方法也比较简单,只是简单的给标志位赋值即可,我们这个程序是基于Oserver模式的简化版本,HttpServer本身是一个被观察的对象(Subject),当这个Subject有变化时(获取到客户端请求时)要通知监听器(我们的RecordHandler)去作操作(写数据库还是写文件或是直接控制台输出),极大的增加了系统的灵活性和易测试性
HttpServer类代码
- packagecom.crazycoder2010.socket;
- importjava.io.BufferedReader;
- importjava.io.IOException;
- importjava.io.InputStreamReader;
- importjava.io.PrintWriter;
- importjava.net.ServerSocket;
- importjava.net.Socket;
- importjava.sql.Date;
- importjava.util.ArrayList;
- importjava.util.List;
- /**
- *服务器监听对象,对某个端口进行监听,基于线程的实现
- *
- *@authorKevin
- *
- */
- publicclassHttpServerimplementsRunnable{
- /**
- *服务器监听
- */
- privateServerSocketserverSocket;
- /**
- *标志位,表示当前服务器是否正在运行
- */
- privatebooleanisRunning;
- /**
- *观察者
- */
- privateList<RecordHandler>recordHandlers=newArrayList<RecordHandler>();
- publicHttpServer(intport){
- try{
- serverSocket=newServerSocket(port);
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- publicvoidstop(){
- this.isRunning=false;
- }
- publicvoidstart(){
- this.isRunning=true;
- newThread(this).start();
- }
- @Override
- publicvoidrun(){
- while(isRunning){//一直监听,直到受到停止的命令
- Socketsocket=null;
- try{
- socket=serverSocket.accept();//如果没有请求,会一直hold在这里等待,有客户端请求的时候才会继续往下执行
- //log
- BufferedReaderbufferedReader=newBufferedReader(
- newInputStreamReader(socket.getInputStream()));//获取输入流(请求)
- StringBuilderstringBuilder=newStringBuilder();
- Stringline=null;
- while((line=bufferedReader.readLine())!=null
- &&!line.equals("")){//得到请求的内容,注意这里作两个判断非空和""都要,只判断null会有问题
- stringBuilder.append(line).append("/n");
- }
- Recordrecord=newRecord();
- record.setRecord(stringBuilder.toString());
- record.setVisitDate(newDate(System.currentTimeMillis()));
- notifyRecordHandlers(record);//通知日志记录者对日志作操作
- //echo
- PrintWriterprintWriter=newPrintWriter(
- socket.getOutputStream(),true);//这里第二个参数表示自动刷新缓存
- doEcho(printWriter,record);//将日志输出到浏览器
- //release
- printWriter.close();
- bufferedReader.close();
- socket.close();
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- }
- /**
- *将得到的信写回客户端
- *
- *@paramprintWriter
- *@paramrecord
- */
- privatevoiddoEcho(PrintWriterprintWriter,Recordrecord){
- printWriter.write(record.getRecord());
- }
- /**
- *通知已经注册的监听者做处理
- *
- *@paramrecord
- */
- privatevoidnotifyRecordHandlers(Recordrecord){
- for(RecordHandlerrecordHandler:this.recordHandlers){
- recordHandler.handleRecord(record);
- }
- }
- /**
- *添加一个监听器
- *
- *@paramrecordHandler
- */
- publicvoidaddRecordHandler(RecordHandlerrecordHandler){
- this.recordHandlers.add(recordHandler);
- }
- }
Record类非常简单,只是作为参数传递的对象来用
- packagecom.crazycoder2010.socket;
- importjava.sql.Date;
- publicclassRecord{
- privateintid;
- privateStringrecord;
- privateDatevisitDate;
- publicintgetId(){
- returnid;
- }
- publicvoidsetId(intid){
- this.id=id;
- }
- publicStringgetRecord(){
- returnrecord;
- }
- publicvoidsetRecord(Stringrecord){
- this.record=record;
- }
- publicDategetVisitDate(){
- returnvisitDate;
- }
- publicvoidsetVisitDate(DatevisitDate){
- this.visitDate=visitDate;
- }
- }
RecordHandler接口,统一监听接口,非常简单
- packagecom.crazycoder2010.socket;
- /**
- *获取到访问信息后的处理接口
- *@authorKevin
- *
- */
- publicinterfaceRecordHandler{
- publicvoidhandleRecord(Recordrecord);
- }
ConsoleRecordHandler实现,直接System打印输出,在我们作测试时非常有用
- packagecom.crazycoder2010.socket;
- publicclassConsoleRecordHandlerimplementsRecordHandler{
- @Override
- publicvoidhandleRecord(Recordrecord){
- System.out.println("@@@@@@@");
- System.out.println(record.getRecord());
- }
- }
MysqlRecordHandler,数据库实现,定义了要对数据库操作所需要的几个基本属性url,username,password,加载驱动的程序我们放在了静态代码短中,这个东东嘛,只要加载一次就ok了
- packagecom.crazycoder2010.socket;
- importjava.sql.Connection;
- importjava.sql.PreparedStatement;
- importjava.sql.SQLException;
- publicclassMysqlRecordHandlerimplementsRecordHandler{
- static{
- try{
- Class.forName("com.mysql.jdbc.Driver");
- }catch(ClassNotFoundExceptione){
- e.printStackTrace();
- }
- }
- privatestaticfinalStringNEW_RECORD="insertintolog(record,visit_date)values(?,?)";
- /**
- *数据库访问url
- */
- privateStringurl;
- /**
- *数据库用户名
- */
- privateStringusername;
- /**
- *数据库密码
- */
- privateStringpassword;
- publicvoidsetUrl(Stringurl){
- this.url=url;
- }
- publicvoidsetUsername(Stringusername){
- this.username=username;
- }
- publicvoidsetPassword(Stringpassword){
- this.password=password;
- }
- @Override
- publicvoidhandleRecord(Recordrecord){
- Connectionconnection=ConnectionFactory.getConnection(url,username,
- password);
- PreparedStatementpreparedStatement=null;
- try{
- preparedStatement=connection.prepareStatement(NEW_RECORD);
- preparedStatement.setString(1,record.getRecord());
- preparedStatement.setDate(2,record.getVisitDate());
- preparedStatement.executeUpdate();
- }catch(SQLExceptione){
- e.printStackTrace();
- }finally{
- ConnectionFactory.release(preparedStatement);
- ConnectionFactory.release(connection);
- }
- }
- }
ConnectionFactory类,我们的数据库连接工厂类,定义了几个常用的方法,把数据库连接独立到外部单独类的好处在于,我们可以很灵活的替换连接的生成方式--如我们可以从连接池中获取一个,而我们的数据库操作却只关注Connection本身,从而达到动静分离的效果
- packagecom.crazycoder2010.socket;
- importjava.sql.Connection;
- importjava.sql.DriverManager;
- importjava.sql.PreparedStatement;
- importjava.sql.SQLException;
- /**
- *创建数据库连接的工厂类
- *
- *@authorKevin
- *
- */
- publicclassConnectionFactory{
- publicstaticConnectiongetConnection(Stringurl,Stringusername,
- Stringpassword){
- try{
- returnDriverManager.getConnection(url,username,password);
- }catch(SQLExceptione){
- e.printStackTrace();
- }
- returnnull;
- }
- /**
- *释放连接
- *
- *@paramconnection
- */
- publicstaticvoidrelease(Connectionconnection){
- if(connection!=null){
- try{
- connection.close();
- connection=null;
- }catch(SQLExceptione){
- e.printStackTrace();
- }
- }
- }
- /**
- *关闭查询语句
- *
- *@parampreparedStatement
- */
- publicstaticvoidrelease(PreparedStatementpreparedStatement){
- if(preparedStatement!=null){
- try{
- preparedStatement.close();
- preparedStatement=null;
- }catch(SQLExceptione){
- e.printStackTrace();
- }
- }
- }
- }
init.sql我们的数据库建表脚本,只是为了演示,一个表就好
- CREATETABLE`logs`.`log`(
- `id`INT(10)NOTNULLAUTO_INCREMENT,
- `record`VARCHAR(1024)NOTNULL,
- `visit_date`DATETIMENOTNULL,
- PRIMARYKEY(`id`)
- )
- ENGINE=MyISAM;
AppLuancher类,是时候把这几个模块高到一起跑起来的时候了,我们首先创建了一个服务器端,然后给服务器创建了两个监听器,然后启动服务器,这个时候我们的HttpServer已经开始监听7777端口了!
- packagecom.crazycoder2010.socket;
- publicclassAppLauncher{
- /**
- *@paramargs
- */
- publicstaticvoidmain(String[]args){
- HttpServerhttpServer=newHttpServer(7777);
- httpServer.addRecordHandler(newConsoleRecordHandler());
- httpServer.addRecordHandler(createMysqlHandler());
- httpServer.start();
- }
- privatestaticRecordHandlercreateMysqlHandler(){
- MysqlRecordHandlerhandler=newMysqlRecordHandler();
- handler.setUrl("jdbc:mysql://localhost:3306/logs");
- handler.setUsername("root");
- handler.setPassword("");
- returnhandler;
- }
- }
打开浏览器,输入http://localhost:7777我们看到控制台输出了一堆的文字
GET / HTTP/1.1
Host: localhost:7777
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
再去查以下我们的数据库,呵呵,也有了,再看看我们的浏览器上是否也把这些信息同样显示出来了~~
总结一下
麻雀虽小,五脏俱全,可能有人说这个小程序高这么多类干吗,我在main函数里一下子不久写完了吗?的确很多人这么搞,但是我不赞同,一个小东西,如果你是报者学习的姿态,一种不把他当玩具的心态来设计它时,你就会比别人多想一步,设计模式,封装变化,单一职责,这些东东不能让他们一直留在大学的课本里,而是有意识的去在实践中运用--实践是检验真理的唯一标准,经验来源于积累
一个基于Socket的http请求监听程序实现
原文地址:http://www.cnblogs.com/csguo/p/7542374.html