最近看了Java的IO包源碼,對BIO有了較深刻的理解。Socket編程其實也是基於IO流操做,而且其流操做都是阻塞的,就想着寫一個Socket程序並對其一步一步優化,來加深對IO的理解。本文主要從簡單的Socket鏈接開始,一步一步優化,最後使用線程池等技術提升併發。Socket源碼本篇未涉及,等有時間我再研究一番。java
Socket編程的基本流程以下圖(圖片來自網絡),一個IP地址和一個端口號稱爲一個套接字(socket)。 spring
以下,最基本的客戶端發送消息,服務端接收消息輸入。須要注意的是,因爲中文的utf8編碼是3個字節,若是使用buffer來分段接收字節流,可能致使亂碼。另外,read()是堵塞的,若是不判斷read() == -1來表示結束,那麼read()方法會一直堵塞。編程
package me.zebin.demo.javaio;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class JavaioApplicationTests {
@Test
public void server() throws Exception {
// 指定端口
ServerSocket ss = new ServerSocket(9999);
System.out.println("server starting...");
// 等待鏈接
Socket s = ss.accept();
// 獲取輸入流,接收客戶端的消息
InputStream is = s.getInputStream();
// 緩存buffer,utf8編碼中文是3個字節,這裏也但是使用BufferedReader解碼
byte[] buffer = new byte[5];
while(true){
int cnt = is.read(buffer);
// 若是不判斷流結束,上面的read()讀不到數據會一直堵塞
if(cnt == -1){
break;
}
String str = new String(buffer, 0, cnt, "utf8");
System.out.println(str);
}
s.close();
ss.close();
}
@Test
public void client() throws Exception{
// 指定端口
Socket s = new Socket("127.0.0.1", 9999);
// 獲取輸出流,向服務端發消息
OutputStream os = s.getOutputStream();
// 發送消息,utf8編碼中文是3個字節,服務端使用buffer可能致使亂碼
String str = "我是客戶端";
os.write(str.getBytes("utf8"));
s.close();
}
}
複製代碼
以上程序,若是buffer設置爲5,運行結果以下,出現亂碼。 數組
@Test
public void server() throws Exception {
// 指定端口
ServerSocket ss = new ServerSocket(9999);
System.out.println("server starting...");
// 等待鏈接
Socket s = ss.accept();
// 獲取輸入流,接收客戶端的消息
InputStream is = s.getInputStream();
// 輸入字節流封裝爲Scanner,讀取整行
Scanner sc = new Scanner(is, "utf8");
while (sc.hasNextLine()){
System.out.println(sc.nextLine());
}
s.close();
ss.close();
}
複製代碼
運行結果以下,沒有亂碼了。 緩存
上面的版本有一個弊端,就是一個服務器只能提供給一個客戶端進行鏈接,若是將鏈接的用線程處理,服務器能夠處理更多的客戶端鏈接,代碼以下:服務器
@Test
public void server() throws Exception {
// 指定端口
ServerSocket ss = new ServerSocket(9998);
System.out.println("server starting...");
while(true){
// 等待鏈接
Socket s = ss.accept();
System.out.println("得到鏈接");
Thread t = new Thread(new ServerThread(s));
t.start();
}
}
class ServerThread implements Runnable{
private Socket s;
ServerThread(Socket s){
this.s = s;
}
@Override
public void run(){
// 獲取輸入流,接收客戶端的消息
InputStream is = null;
try {
is = s.getInputStream();
// 使用Scanner封裝
Scanner sc = new Scanner(is, "utf8");
while (sc.hasNextLine()){
System.out.println(sc.nextLine());
}
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
以上多線程版本咱們使用了多線程來處理併發,不過線程的建立和銷燬都會消耗大量的資源和時間,同時,高併發下會建立很是多的線程,且不說操做系統能開啓的線程數有限,操做系統維護和切換大量的線程也會很是耗時。因此使用線程池,只用4個線程,用隊列將未執行到的線程排隊處理,減小了線程數量,同時也避免了建立和銷燬線程帶來的性能問題。網絡
@Test
public void server() throws Exception {
// 指定端口
ServerSocket ss = new ServerSocket(9998);
System.out.println("server starting...");
// 建立線程隊列
BlockingQueue bq = new ArrayBlockingQueue(100);
// 拒絕策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
Executor executor = new ThreadPoolExecutor(4, 8, 1, TimeUnit.MINUTES, bq, handler);
while(true){
// 等待鏈接
Socket s = ss.accept();
System.out.println("得到鏈接");
Thread t = new Thread(new ServerThread(s));
executor.execute(t);
}
}
複製代碼
以上,本篇結束。多線程