首先來看以下我們的需求:
用java編寫一個監聽程序,監聽指定的端口,通過瀏覽器如http://localhost:7777來訪問時,可以把請求到的內容記錄下來,記錄可以存文件,sqlit,mysql數據庫,然后把接受到的信息在瀏覽器中顯示出來
要點:
Socket,線程,數據庫,IO操作,觀察者模式
來看下我們如何來設計這個小系統,這個系統包含三部分的內容,一個是監聽端口,二是記錄日志,三是數據回顯,端口監聽第一想到的就是Socket編程了,數據回顯也是一樣的,無非是把當前請求客戶端的socket獲取到,然后把消息通過流輸出出去,日志的記錄因為是要多種實現策略,這里我們使用了一個觀察者模式來實現,服務器可以添加任意多個觀察着,因此有着很靈活的擴展性,在實例程序中我們分別提供了ConsoleRecordHandler--直接把獲取到的信息打印到控制台,和存放數據庫的方式-MysqlRecordHandler,當然你也可以分別提供基於文件的實現。
首先來看我們系統的類圖
HttpServer類是我們的核心類,他實現了Runnable接口,因此有着更高的性能,在循環中不斷的去輪詢指定端口,構造方法比較簡單,只需要一個要監聽的端口號即可,還有兩個用於觸發監聽和停止程序運行的方法stop()&start(),這兩個方法也比較簡單,只是簡單的給標志位賦值即可,我們這個程序是基於Oserver模式的簡化版本,HttpServer本身是一個被觀察的對象(Subject),當這個Subject有變化時(獲取到客戶端請求時)要通知監聽器(我們的RecordHandler)去作操作(寫數據庫還是寫文件或是直接控制台輸出),極大的增加了系統的靈活性和易測試性
HttpServer類代碼
- package com.crazycoder2010.socket;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.PrintWriter;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.sql.Date;
- import java.util.ArrayList;
- import java.util.List;
- /**
- * 服務器監聽對象,對某個端口進行監聽,基於線程的實現
- *
- * @author Kevin
- *
- */
- public class HttpServer implements Runnable {
- /**
- * 服務器監聽
- */
- private ServerSocket serverSocket;
- /**
- * 標志位,表示當前服務器是否正在運行
- */
- private boolean isRunning;
- /**
- * 觀察者
- */
- private List<RecordHandler> recordHandlers = new ArrayList<RecordHandler>();
- public HttpServer(int port) {
- try {
- serverSocket = new ServerSocket(port);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public void stop() {
- this.isRunning = false;
- }
- public void start() {
- this.isRunning = true;
- new Thread(this).start();
- }
- @Override
- public void run() {
- while (isRunning) {//一直監聽,直到受到停止的命令
- Socket socket = null;
- try {
- socket = serverSocket.accept();//如果沒有請求,會一直hold在這里等待,有客戶端請求的時候才會繼續往下執行
- // log
- BufferedReader bufferedReader = new BufferedReader(
- new InputStreamReader(socket.getInputStream()));//獲取輸入流(請求)
- StringBuilder stringBuilder = new StringBuilder();
- String line = null;
- while ((line = bufferedReader.readLine()) != null
- && !line.equals("")) {//得到請求的內容,注意這里作兩個判斷非空和""都要,只判斷null會有問題
- stringBuilder.append(line).append("/n");
- }
- Record record = new Record();
- record.setRecord(stringBuilder.toString());
- record.setVisitDate(new Date(System.currentTimeMillis()));
- notifyRecordHandlers(record);//通知日志記錄者對日志作操作
- // echo
- PrintWriter printWriter = new PrintWriter(
- socket.getOutputStream(), true);//這里第二個參數表示自動刷新緩存
- doEcho(printWriter, record);//將日志輸出到瀏覽器
- // release
- printWriter.close();
- bufferedReader.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * 將得到的信寫回客戶端
- *
- * @param printWriter
- * @param record
- */
- private void doEcho(PrintWriter printWriter, Record record) {
- printWriter.write(record.getRecord());
- }
- /**
- * 通知已經注冊的監聽者做處理
- *
- * @param record
- */
- private void notifyRecordHandlers(Record record) {
- for (RecordHandler recordHandler : this.recordHandlers) {
- recordHandler.handleRecord(record);
- }
- }
- /**
- * 添加一個監聽器
- *
- * @param recordHandler
- */
- public void addRecordHandler(RecordHandler recordHandler) {
- this.recordHandlers.add(recordHandler);
- }
- }
Record類非常簡單,只是作為參數傳遞的對象來用
- package com.crazycoder2010.socket;
- import java.sql.Date;
- public class Record {
- private int id;
- private String record;
- private Date visitDate;
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- public String getRecord() {
- return record;
- }
- public void setRecord(String record) {
- this.record = record;
- }
- public Date getVisitDate() {
- return visitDate;
- }
- public void setVisitDate(Date visitDate) {
- this.visitDate = visitDate;
- }
- }
RecordHandler接口,統一監聽接口,非常簡單
- package com.crazycoder2010.socket;
- /**
- * 獲取到訪問信息后的處理接口
- * @author Kevin
- *
- */
- public interface RecordHandler {
- public void handleRecord(Record record);
- }
ConsoleRecordHandler實現,直接System打印輸出,在我們作測試時非常有用
- package com.crazycoder2010.socket;
- public class ConsoleRecordHandler implements RecordHandler {
- @Override
- public void handleRecord(Record record) {
- System.out.println("@@@@@@@");
- System.out.println(record.getRecord());
- }
- }
MysqlRecordHandler,數據庫實現,定義了要對數據庫操作所需要的幾個基本屬性url,username,password,加載驅動的程序我們放在了靜態代碼短中,這個東東嘛,只要加載一次就ok了
- package com.crazycoder2010.socket;
- import java.sql.Connection;
- import java.sql.PreparedStatement;
- import java.sql.SQLException;
- public class MysqlRecordHandler implements RecordHandler {
- static {
- try {
- Class.forName("com.mysql.jdbc.Driver");
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- private static final String NEW_RECORD = "insert into log(record,visit_date) values (?,?)";
- /**
- * 數據庫訪問url
- */
- private String url;
- /**
- * 數據庫用戶名
- */
- private String username;
- /**
- * 數據庫密碼
- */
- private String password;
- public void setUrl(String url) {
- this.url = url;
- }
- public void setUsername(String username) {
- this.username = username;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- @Override
- public void handleRecord(Record record) {
- Connection connection = ConnectionFactory.getConnection(url, username,
- password);
- PreparedStatement preparedStatement = null;
- try {
- preparedStatement = connection.prepareStatement(NEW_RECORD);
- preparedStatement.setString(1, record.getRecord());
- preparedStatement.setDate(2, record.getVisitDate());
- preparedStatement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- } finally {
- ConnectionFactory.release(preparedStatement);
- ConnectionFactory.release(connection);
- }
- }
- }
ConnectionFactory類,我們的數據庫連接工廠類,定義了幾個常用的方法,把數據庫連接獨立到外部單獨類的好處在於,我們可以很靈活的替換連接的生成方式--如我們可以從連接池中獲取一個,而我們的數據庫操作卻只關注Connection本身,從而達到動靜分離的效果
- package com.crazycoder2010.socket;
- import java.sql.Connection;
- import java.sql.DriverManager;
- import java.sql.PreparedStatement;
- import java.sql.SQLException;
- /**
- * 創建數據庫連接的工廠類
- *
- * @author Kevin
- *
- */
- public class ConnectionFactory {
- public static Connection getConnection(String url, String username,
- String password) {
- try {
- return DriverManager.getConnection(url, username, password);
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return null;
- }
- /**
- * 釋放連接
- *
- * @param connection
- */
- public static void release(Connection connection) {
- if (connection != null) {
- try {
- connection.close();
- connection = null;
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * 關閉查詢語句
- *
- * @param preparedStatement
- */
- public static void release(PreparedStatement preparedStatement) {
- if (preparedStatement != null) {
- try {
- preparedStatement.close();
- preparedStatement = null;
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
- }
init.sql我們的數據庫建表腳本,只是為了演示,一個表就好
- CREATE TABLE `logs`.`log` (
- `id` INT(10) NOT NULL AUTO_INCREMENT,
- `record` VARCHAR(1024) NOT NULL,
- `visit_date` DATETIME NOT NULL,
- PRIMARY KEY (`id`)
- )
- ENGINE = MyISAM;
AppLuancher類,是時候把這幾個模塊高到一起跑起來的時候了,我們首先創建了一個服務器端,然后給服務器創建了兩個監聽器,然后啟動服務器,這個時候我們的HttpServer已經開始監聽7777端口了!
- package com.crazycoder2010.socket;
- public class AppLauncher {
- /**
- * @param args
- */
- public static void main(String[] args) {
- HttpServer httpServer = new HttpServer(7777);
- httpServer.addRecordHandler(new ConsoleRecordHandler());
- httpServer.addRecordHandler(createMysqlHandler());
- httpServer.start();
- }
- private static RecordHandler createMysqlHandler(){
- MysqlRecordHandler handler = new MysqlRecordHandler();
- handler.setUrl("jdbc:mysql://localhost:3306/logs");
- handler.setUsername("root");
- handler.setPassword("");
- return handler;
- }
- }
打開瀏覽器,輸入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函數里一下子不久寫完了嗎?的確很多人這么搞,但是我不贊同,一個小東西,如果你是報者學習的姿態,一種不把他當玩具的心態來設計它時,你就會比別人多想一步,設計模式,封裝變化,單一職責,這些東東不能讓他們一直留在大學的課本里,而是有意識的去在實踐中運用--實踐是檢驗真理的唯一標准,經驗來源於積累