一個基於Socket的http請求監聽程序實現


首先來看以下我們的需求:

用java編寫一個監聽程序,監聽指定的端口,通過瀏覽器如http://localhost:7777來訪問時,可以把請求到的內容記錄下來,記錄可以存文件,sqlit,mysql數據庫,然后把接受到的信息在瀏覽器中顯示出來

要點:

Socket,線程,數據庫,IO操作,觀察者模式

來看下我們如何來設計這個小系統,這個系統包含三部分的內容,一個是監聽端口,二是記錄日志,三是數據回顯,端口監聽第一想到的就是Socket編程了,數據回顯也是一樣的,無非是把當前請求客戶端的socket獲取到,然后把消息通過流輸出出去,日志的記錄因為是要多種實現策略,這里我們使用了一個觀察者模式來實現,服務器可以添加任意多個觀察着,因此有着很靈活的擴展性,在實例程序中我們分別提供了ConsoleRecordHandler--直接把獲取到的信息打印到控制台,和存放數據庫的方式-MysqlRecordHandler,當然你也可以分別提供基於文件的實現。

首先來看我們系統的類圖

 

HttpServer系統類圖

 

HttpServer類是我們的核心類,他實現了Runnable接口,因此有着更高的性能,在循環中不斷的去輪詢指定端口,構造方法比較簡單,只需要一個要監聽的端口號即可,還有兩個用於觸發監聽和停止程序運行的方法stop()&start(),這兩個方法也比較簡單,只是簡單的給標志位賦值即可,我們這個程序是基於Oserver模式的簡化版本,HttpServer本身是一個被觀察的對象(Subject),當這個Subject有變化時(獲取到客戶端請求時)要通知監聽器(我們的RecordHandler)去作操作(寫數據庫還是寫文件或是直接控制台輸出),極大的增加了系統的靈活性和易測試性

HttpServer類代碼

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. import java.io.BufferedReader;  
  3. import java.io.IOException;  
  4. import java.io.InputStreamReader;  
  5. import java.io.PrintWriter;  
  6. import java.net.ServerSocket;  
  7. import java.net.Socket;  
  8. import java.sql.Date;  
  9. import java.util.ArrayList;  
  10. import java.util.List;  
  11. /** 
  12.  * 服務器監聽對象,對某個端口進行監聽,基於線程的實現 
  13.  *  
  14.  * @author Kevin 
  15.  *  
  16.  */  
  17. public class HttpServer implements Runnable {  
  18.     /** 
  19.      * 服務器監聽 
  20.      */  
  21.     private ServerSocket serverSocket;  
  22.     /** 
  23.      * 標志位,表示當前服務器是否正在運行 
  24.      */  
  25.     private boolean isRunning;  
  26.     /** 
  27.      * 觀察者 
  28.      */  
  29.     private List<RecordHandler> recordHandlers = new ArrayList<RecordHandler>();  
  30.     public HttpServer(int port) {  
  31.         try {  
  32.             serverSocket = new ServerSocket(port);  
  33.         } catch (IOException e) {  
  34.             e.printStackTrace();  
  35.         }  
  36.     }  
  37.     public void stop() {  
  38.         this.isRunning = false;  
  39.     }  
  40.     public void start() {  
  41.         this.isRunning = true;  
  42.         new Thread(this).start();  
  43.     }  
  44.     @Override  
  45.     public void run() {  
  46.         while (isRunning) {//一直監聽,直到受到停止的命令  
  47.             Socket socket = null;  
  48.             try {  
  49.                 socket = serverSocket.accept();//如果沒有請求,會一直hold在這里等待,有客戶端請求的時候才會繼續往下執行  
  50.                 // log  
  51.                 BufferedReader bufferedReader = new BufferedReader(  
  52.                         new InputStreamReader(socket.getInputStream()));//獲取輸入流(請求)  
  53.                 StringBuilder stringBuilder = new StringBuilder();  
  54.                 String line = null;  
  55.                 while ((line = bufferedReader.readLine()) != null  
  56.                         && !line.equals("")) {//得到請求的內容,注意這里作兩個判斷非空和""都要,只判斷null會有問題  
  57.                     stringBuilder.append(line).append("/n");  
  58.                 }  
  59.                 Record record = new Record();  
  60.                 record.setRecord(stringBuilder.toString());  
  61.                 record.setVisitDate(new Date(System.currentTimeMillis()));  
  62.                 notifyRecordHandlers(record);//通知日志記錄者對日志作操作  
  63.                 // echo  
  64.                 PrintWriter printWriter = new PrintWriter(  
  65.                         socket.getOutputStream(), true);//這里第二個參數表示自動刷新緩存  
  66.                 doEcho(printWriter, record);//將日志輸出到瀏覽器  
  67.                 // release  
  68.                 printWriter.close();  
  69.                 bufferedReader.close();  
  70.                 socket.close();  
  71.             } catch (IOException e) {  
  72.                 e.printStackTrace();  
  73.             }  
  74.         }  
  75.     }  
  76.     /** 
  77.      * 將得到的信寫回客戶端 
  78.      *  
  79.      * @param printWriter 
  80.      * @param record 
  81.      */  
  82.     private void doEcho(PrintWriter printWriter, Record record) {  
  83.         printWriter.write(record.getRecord());  
  84.     }  
  85.     /** 
  86.      * 通知已經注冊的監聽者做處理 
  87.      *  
  88.      * @param record 
  89.      */  
  90.     private void notifyRecordHandlers(Record record) {  
  91.         for (RecordHandler recordHandler : this.recordHandlers) {  
  92.             recordHandler.handleRecord(record);  
  93.         }  
  94.     }  
  95.     /** 
  96.      * 添加一個監聽器 
  97.      *  
  98.      * @param recordHandler 
  99.      */  
  100.     public void addRecordHandler(RecordHandler recordHandler) {  
  101.         this.recordHandlers.add(recordHandler);  
  102.     }  
  103. }  

 

 

Record類非常簡單,只是作為參數傳遞的對象來用

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. import java.sql.Date;  
  3. public class Record {  
  4.     private int id;  
  5.     private String record;  
  6.     private Date visitDate;  
  7.     public int getId() {  
  8.         return id;  
  9.     }  
  10.     public void setId(int id) {  
  11.         this.id = id;  
  12.     }  
  13.     public String getRecord() {  
  14.         return record;  
  15.     }  
  16.     public void setRecord(String record) {  
  17.         this.record = record;  
  18.     }  
  19.     public Date getVisitDate() {  
  20.         return visitDate;  
  21.     }  
  22.     public void setVisitDate(Date visitDate) {  
  23.         this.visitDate = visitDate;  
  24.     }  
  25. }  

 

 

RecordHandler接口,統一監聽接口,非常簡單

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. /** 
  3.  * 獲取到訪問信息后的處理接口 
  4.  * @author Kevin 
  5.  * 
  6.  */  
  7. public interface RecordHandler {  
  8.     public void handleRecord(Record record);  
  9. }  

 

 

ConsoleRecordHandler實現,直接System打印輸出,在我們作測試時非常有用

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. public class ConsoleRecordHandler implements RecordHandler {  
  3.     @Override  
  4.     public void handleRecord(Record record) {  
  5.         System.out.println("@@@@@@@");  
  6.         System.out.println(record.getRecord());  
  7.     }  
  8. }  

 

 

MysqlRecordHandler,數據庫實現,定義了要對數據庫操作所需要的幾個基本屬性url,username,password,加載驅動的程序我們放在了靜態代碼短中,這個東東嘛,只要加載一次就ok了

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. import java.sql.Connection;  
  3. import java.sql.PreparedStatement;  
  4. import java.sql.SQLException;  
  5. public class MysqlRecordHandler implements RecordHandler {  
  6.     static {  
  7.         try {  
  8.             Class.forName("com.mysql.jdbc.Driver");  
  9.         } catch (ClassNotFoundException e) {  
  10.             e.printStackTrace();  
  11.         }  
  12.     }  
  13.     private static final String NEW_RECORD = "insert into log(record,visit_date) values (?,?)";  
  14.     /** 
  15.      * 數據庫訪問url 
  16.      */  
  17.     private String url;  
  18.     /** 
  19.      * 數據庫用戶名 
  20.      */  
  21.     private String username;  
  22.     /** 
  23.      * 數據庫密碼 
  24.      */  
  25.     private String password;  
  26.     public void setUrl(String url) {  
  27.         this.url = url;  
  28.     }  
  29.     public void setUsername(String username) {  
  30.         this.username = username;  
  31.     }  
  32.     public void setPassword(String password) {  
  33.         this.password = password;  
  34.     }  
  35.     @Override  
  36.     public void handleRecord(Record record) {  
  37.         Connection connection = ConnectionFactory.getConnection(url, username,  
  38.                 password);  
  39.         PreparedStatement preparedStatement = null;  
  40.         try {  
  41.             preparedStatement = connection.prepareStatement(NEW_RECORD);  
  42.             preparedStatement.setString(1, record.getRecord());  
  43.             preparedStatement.setDate(2, record.getVisitDate());  
  44.             preparedStatement.executeUpdate();  
  45.         } catch (SQLException e) {  
  46.             e.printStackTrace();  
  47.         } finally {  
  48.             ConnectionFactory.release(preparedStatement);  
  49.             ConnectionFactory.release(connection);  
  50.         }  
  51.     }  
  52. }  

 

 

ConnectionFactory類,我們的數據庫連接工廠類,定義了幾個常用的方法,把數據庫連接獨立到外部單獨類的好處在於,我們可以很靈活的替換連接的生成方式--如我們可以從連接池中獲取一個,而我們的數據庫操作卻只關注Connection本身,從而達到動靜分離的效果

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. import java.sql.Connection;  
  3. import java.sql.DriverManager;  
  4. import java.sql.PreparedStatement;  
  5. import java.sql.SQLException;  
  6. /** 
  7.  * 創建數據庫連接的工廠類 
  8.  *  
  9.  * @author Kevin 
  10.  *  
  11.  */  
  12. public class ConnectionFactory {  
  13.     public static Connection getConnection(String url, String username,  
  14.             String password) {  
  15.         try {  
  16.             return DriverManager.getConnection(url, username, password);  
  17.         } catch (SQLException e) {  
  18.             e.printStackTrace();  
  19.         }  
  20.         return null;  
  21.     }  
  22.     /** 
  23.      * 釋放連接 
  24.      *  
  25.      * @param connection 
  26.      */  
  27.     public static void release(Connection connection) {  
  28.         if (connection != null) {  
  29.             try {  
  30.                 connection.close();  
  31.                 connection = null;  
  32.             } catch (SQLException e) {  
  33.                 e.printStackTrace();  
  34.             }  
  35.         }  
  36.     }  
  37.     /** 
  38.      * 關閉查詢語句 
  39.      *  
  40.      * @param preparedStatement 
  41.      */  
  42.     public static void release(PreparedStatement preparedStatement) {  
  43.         if (preparedStatement != null) {  
  44.             try {  
  45.                 preparedStatement.close();  
  46.                 preparedStatement = null;  
  47.             } catch (SQLException e) {  
  48.                 e.printStackTrace();  
  49.             }  
  50.         }  
  51.     }  
  52. }  

 

 

init.sql我們的數據庫建表腳本,只是為了演示,一個表就好

 

[java]  view plain  copy
 
 print?
  1. CREATE TABLE `logs`.`log` (  
  2.   `id` INT(10)  NOT NULL AUTO_INCREMENT,  
  3.   `record` VARCHAR(1024)  NOT NULL,  
  4.   `visit_date` DATETIME  NOT NULL,  
  5.   PRIMARY KEY (`id`)  
  6. )  
  7. ENGINE = MyISAM;  

 

 

AppLuancher類,是時候把這幾個模塊高到一起跑起來的時候了,我們首先創建了一個服務器端,然后給服務器創建了兩個監聽器,然后啟動服務器,這個時候我們的HttpServer已經開始監聽7777端口了!

 

[java]  view plain  copy
 
 print?
  1. package com.crazycoder2010.socket;  
  2. public class AppLauncher {  
  3.     /** 
  4.      * @param args 
  5.      */  
  6.     public static void main(String[] args) {  
  7.         HttpServer httpServer = new HttpServer(7777);  
  8.         httpServer.addRecordHandler(new ConsoleRecordHandler());  
  9.         httpServer.addRecordHandler(createMysqlHandler());  
  10.         httpServer.start();  
  11.     }  
  12.     private static RecordHandler createMysqlHandler(){  
  13.         MysqlRecordHandler handler = new MysqlRecordHandler();  
  14.         handler.setUrl("jdbc:mysql://localhost:3306/logs");  
  15.         handler.setUsername("root");  
  16.         handler.setPassword("");  
  17.         return handler;  
  18.     }  
  19. }  

 

 

 

打開瀏覽器,輸入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函數里一下子不久寫完了嗎?的確很多人這么搞,但是我不贊同,一個小東西,如果你是報者學習的姿態,一種不把他當玩具的心態來設計它時,你就會比別人多想一步,設計模式,封裝變化,單一職責,這些東東不能讓他們一直留在大學的課本里,而是有意識的去在實踐中運用--實踐是檢驗真理的唯一標准,經驗來源於積累


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM