每一種技術的出現(xiàn)汁针,都是為了解決某一個或者某一類問題。讓我們先來了解問題的產生粹胯。
問題:
使用socket通信實現(xiàn)如下:
1.client連接server
2.client發(fā)送"Hi Server,I am client."
3.server收到消息在控制臺的打印下隧,并回復"Hi client,I am Server."
4.client收到消息在控制臺打印。
5.client斷開連接。
1.Simple Solution(方式一)
直接貼代碼了
/**
* @description: SimpleSolution server
* @author: sanjin
* @date: 2019/7/8 11:33
*/
public class Server {
public static void main(String[] args) {
// 服務端占用端口
int port = 8000;
// 創(chuàng)建 serversocker
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
if (serverSocket != null) {
while (true) {
InputStream is = null;
OutputStream os = null;
Socket client = null;
try {
// accept()方法會阻塞殷勘,直到有client連接后才會執(zhí)行后面的代碼
client = serverSocket.accept();
is = client.getInputStream();
os = client.getOutputStream();
// 3.server收到消息在控制臺的打印此再,并回復"Hi client,I am Server."
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
// 服務端回復客戶端消息
os.write("Hi client,I am Server.".getBytes());
os.flush(); // 刷新緩存玲销,避免消息沒有發(fā)送出去
client.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 程序異呈淠矗或者執(zhí)行完成,關閉流贤斜,防止占用資源
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
}
/**
* @description: SimpleSolution client
* @author: sanjin
* @date: 2019/7/8 11:33
*/
public class Client {
public static void main(String[] args) {
int port = 8000;
Socket client = null;
InputStream is = null;
OutputStream os = null;
try {
// 1.client連接server
client = new Socket("localhost", port);
is = client.getInputStream();
os = client.getOutputStream();
// 2.client發(fā)送"Hi Server,I am client."
os.write("Hi Server,I am client.".getBytes());
os.flush();
// 調用shutdownOutput()方法表示客戶端傳輸完了數(shù)據(jù)策吠,否則服務端的
// read()方法會一直阻塞
// (你可能會問我這不是寫了 read()!=-1, -1表示的文本文件的結尾字符串,而對于字節(jié)流數(shù)據(jù),
// 是沒有 -1 標識的,這就會使服務端無法判斷客戶端是否發(fā)送完成仰泻,導致read()方法一直阻塞)
client.shutdownOutput();
// 4.client收到消息在控制臺打印埃唯。
int len = 0;
byte[] buffer = new byte[5];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
// 程序異常或者執(zhí)行完成,關閉流,防止占用資源
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (client != null) {
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
程序描述圖:
順便說一下,ProcessOn真的很好用??
關于Socket編程跋理,有幾個注意點:
- 注意使用流時一定要用try-catch-finally,雖然代碼確實有點繁瑣。
2.客戶端如果發(fā)送的使中文恬总,在服務端接收數(shù)據(jù)時候前普,要注意接收方式:
// 接收數(shù)據(jù)方式一
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println(baos.toString());
// 接收數(shù)據(jù)方式一
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream壹堰,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
// 這種方式會導致中文亂碼
System.out.println(new String(buffer, 0, len));
}
如果客戶端傳輸中文使用方式二會導致中文亂碼汁政,這是因為我們在讀取時候緩沖區(qū)大小設置的是5個字節(jié),此處假設客戶端傳輸“小白兔“三個字缀旁,常用的漢字一般占3個字節(jié)记劈。”小白兔“發(fā)送過來后我們的緩沖區(qū)只有5個字節(jié)并巍,沒辦法一次讀取完目木,所以要分二次讀取,第一次讀取5個字節(jié)懊渡,然后立即進行了打印刽射,漢字”小“會被正常打印,但是漢字”白“只讀取了2個字節(jié)剃执,打印就會產生亂碼誓禁。而使用ByteArrayOutputStream
把緩沖區(qū)讀取的字節(jié)全都存放一起,然后一起打印肾档,就不會導致亂碼了摹恰。
3.shutdownOutput()
方法辫继。當客戶端傳輸”Hi Server,I am client.“,服務端接收數(shù)據(jù)并打印出來俗慈,然后向客戶端發(fā)送”"Hi client,I am Server."姑宽。如果不使用shutdownOutput()
方法會使服務端卡在read()方法。這是因為當客戶端數(shù)據(jù)發(fā)送完成后闺阱,服務端的判斷條件
while ((len = is.read(buffer)) != -1)
不成立炮车,因為只有文本文件的末尾是 -1,而字節(jié)流沒有末尾標識,這就導致服務端不知道客戶端有沒有發(fā)送完成酣溃,使得read()方法阻塞瘦穆。所以客戶端發(fā)送完數(shù)據(jù)后需要發(fā)送一個標識來表示”我已經(jīng)發(fā)送完數(shù)據(jù)了“。而shutdownOutput()
方法就是這個標識赊豌。
我們使用socket完成了一個收發(fā)的程序扛或。但是它還存在著問題。
1. 不能同時有多個client連接我們的server
服務端與客戶端連接使用依靠accept()函數(shù)亿絮,而我們的服務端程序是單線程告喊,只能等當前的socket執(zhí)行完成后麸拄,才能接收下一個socket的連接派昧。
假設我們同時又2個client連接server會發(fā)生什么?(因為我們程序簡單拢切,執(zhí)行的很快蒂萎,所以我在server種加了Thread.sleep(50*1000))
現(xiàn)象:第二個client會拋出異常:
下面我們就用多線程解決這個問題。
2.Multithreading Solution(方式二)
我又新加了一個HandlerClient類淮椰,實現(xiàn)Runnable接口五慈,用于處理client連接,Client類的代碼沒有做修改主穗。
/**
* @description: MultithreadingSolution client
* @author: sanjin
* @date: 2019/7/8 11:33
*/
public class Client {
public static void main(String[] args) {
int port = 8000;
Socket client = null;
InputStream is = null;
OutputStream os = null;
try {
// 1.client連接server
client = new Socket("localhost", port);
is = client.getInputStream();
os = client.getOutputStream();
// 2.client發(fā)送"Hi Server,I am client."
os.write("Hi Server,I am client.".getBytes());
os.flush();
// 調用shutdownOutput()方法表示客戶端傳輸完了數(shù)據(jù)泻拦,否則服務端的
// read()方法會一直阻塞
// (你可能會問我這不是寫了 read()!=-1, -1表示的文本文件的結尾字符串,而對于字節(jié)流數(shù)據(jù)忽媒,
// 是沒有 -1 標識的争拐,這就會使服務端無法判斷客戶端是否發(fā)送完成,導致read()方法一直阻塞)
client.shutdownOutput();
// 4.client收到消息在控制臺打印晦雨。
int len = 0;
byte[] buffer = new byte[5];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
// 程序異臣懿埽或者執(zhí)行完成,關閉流闹瞧,防止占用資源
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (client != null) {
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @description: MultithreadingSolution server
* @author: sanjin
* @date: 2019/7/8 11:33
*/
public class Server {
public static void main(String[] args) {
// 服務端占用端口
int port = 8000;
// 創(chuàng)建 serversocker
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
if (serverSocket != null) {
while (true) {
try {
Socket client = serverSocket.accept();
System.out.println("收到client連接绑雄,client地址:"+client.getInetAddress());
new Thread(new HandlerClient(client)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* @description: 用于處理client連接
* @author: sanjin
* @date: 2019/7/8 16:28
*/
public class HandlerClient implements Runnable {
private Socket client;
public HandlerClient(Socket client) {
this.client = client;
}
@Override
public void run() {
InputStream is = null;
OutputStream os = null;
try {
is = client.getInputStream();
os = client.getOutputStream();
// 3.server收到消息在控制臺的打印,并回復"Hi client,I am Server."
byte[] buffer = new byte[5];
int len = 0;
// 使用ByteArrayOutputStream奥邮,避免緩沖區(qū)過小導致中文亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
// 這種方式會導致中文亂碼
// System.out.println(new String(buffer, 0, len));
baos.write(buffer, 0, len);
}
System.out.println(baos.toString());
try {
// 增加任務執(zhí)行時間万牺,用于進行多個client連接測試
Thread.sleep(20*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 服務端回復客戶端消息
os.write("Hi client,I am Server.".getBytes());
os.flush(); // 刷新緩存罗珍,避免消息沒有發(fā)送出去
client.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 程序異常或者執(zhí)行完成杏愤,關閉流靡砌,防止占用資源
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
運行結果:
問題:計算機的CPU資源有限,來一個client就會創(chuàng)建一個線程珊楼,線程完成任務后再進行銷毀通殃,線程的創(chuàng)建、銷毀以及線程上下文的切換會消耗很多CPU的資源厕宗。并且JVM中線程數(shù)過多還有可能拋出內存不足的異常画舌。
所以我們下一步使用線程池來解決這個問題。
程序描述圖:
3.Thread Pool Solution(方式三)
線程池解決方法思路:
我們再方式二已經(jīng)完成了多線程方式代碼已慢,將它修改成線程池方式非常簡單曲聂,我們只需要修改Server類就可以了:
/**
* @description: ThreadPoolSolution server
* @author: sanjin
* @date: 2019/7/8 11:33
*/
public class Server {
// 創(chuàng)建線程池
private static ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
5, // 核心線程數(shù)
10, // 最大線程數(shù)
200, // keep alive 時間
TimeUnit.HOURS, // keep alive 時間單位
new ArrayBlockingQueue<Runnable>(5) // 工作隊列
);
public static void main(String[] args) {
// 服務端占用端口
int port = 8000;
// 創(chuàng)建 serversocker
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
if (serverSocket != null) {
while (true) {
try {
Socket client = serverSocket.accept();
System.out.println("收到client連接,client地址:"+client.getInetAddress());
// 多線程方式
// new Thread(new HandlerClient(client)).start();
// 線程池方式
threadPoolExecutor.execute(new HandlerClient(client));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
不知道大家暈了沒佑惠,我已經(jīng)快不行了朋腋,但還是要明白我們使用多線程的目的:
解決多個client同時連接的問題。
好了膜楷,下面主角登場旭咽。
4.NIO(方式三)
關于JavaNIO有一個非常好的英文資料:http://tutorials.jenkov.com/java-nio/index.html
/**
* @description:
* @author: sanjin
* @date: 2019/7/8 19:56
*/
public class NIOClient {
public static void main(String[] args) {
SocketAddress socketAddress = new InetSocketAddress(8000);
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open(socketAddress);
socketChannel.configureBlocking(false);
if (socketChannel.finishConnect()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 客戶端發(fā)送數(shù)據(jù) "Hi Server,I am client."
buffer.clear();
buffer.put("Hi Server,I am client.".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 客戶端接收服務端數(shù)據(jù)打印在控制臺
buffer.clear();
int len = socketChannel.read(buffer);
while (len > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
buffer.clear();
len = socketChannel.read(buffer);
}
if (len == -1) {
socketChannel.close();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socketChannel != null) {
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @description:
* @author: sanjin
* @date: 2019/7/8 19:56
*/
public class NIOServer {
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
Selector selector = null;
try {
// 初始化一個 serverSocketChannel
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
// 設置serverSocketChannel為非阻塞模式
// 即 select()會立即得到返回
serverSocketChannel.configureBlocking(false);
// 初始化一個 selector
selector = Selector.open();
// 將 serverSocketChannel 與 selector綁定
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 通過操作系統(tǒng)監(jiān)聽變化的socket個數(shù)
// 在windows平臺通過selector監(jiān)聽(輪詢所有的socket進行判斷,效率低)
// 在Linux2.6之后通過epool監(jiān)聽(事件驅動方式赌厅,效率高)
int count = selector.select(3000);
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
if (key.isWritable() && key.isValid()) {
handleWrite(key);
}
if (key.isConnectable()) {
System.out.println("isConnectable = true");
}
iterator.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (serverSocketChannel != null) {
serverSocketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (selector != null) {
selector.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void handleWrite(SelectionKey key) {
// 獲取 client 的 socket
SocketChannel clientChannel = (SocketChannel) key.channel();
// 獲取緩沖區(qū)
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
buffer.put("Hi client,I am Server.".getBytes());
buffer.flip();
try {
while (buffer.hasRemaining()) {
clientChannel.write(buffer);
}
buffer.compact();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleRead(SelectionKey key) {
// 獲取 readable 的客戶端 socketChannel
SocketChannel clientChannel = (SocketChannel) key.channel();
// 讀取客戶端發(fā)送的消息信息,我們已經(jīng)在 acceptable 中設置了緩沖區(qū)
// 所以直接沖緩沖區(qū)讀取信息
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 獲取 client 發(fā)送的消息
try {
int len = clientChannel.read(buffer);
while (len > 0) {
// 設置 limit 位置
buffer.flip();
// 開始讀取數(shù)據(jù)
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}
System.out.println();
// 清除 position 位置
buffer.clear();
// 從新讀取 len
len = clientChannel.read(buffer);
}
if (len == -1) {
clientChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleAccept(SelectionKey key) {
// 獲得 serverSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
try {
// 獲得 socketChannel,就是client的socket
SocketChannel clientChannel = serverSocketChannel.accept();
if (clientChannel == null) return;
// 設置 socketChannel 為無阻塞模式
clientChannel.configureBlocking(false);
// 將其注冊到 selector 中穷绵,設置監(jiān)聽其是否可讀,并分配緩沖區(qū)
clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(512));
} catch (IOException e) {
e.printStackTrace();
}
}
}