Java中的Socket分為普通的Socket和NioSocket。
普通Socket的用法
Java中的網絡通信時通過Socket實現的,Socket分為ServerSocket和Socket兩大類,ServerSocket用於服務器端,可以通過accept方法監聽請求,監聽請求後返回Socket,Socket用於完成具體數據傳輸,客戶端也可以使用Socket發起請求並傳輸數據。ServerSocket的使用可以分為三步:
•創建ServerSocket。ServerSocket的構造方法有5個,其中最方便的是ServerSocket(int port),只需要一個port就可以了。
•調用創建出來的ServerSocket的accept方法進行監聽。accept方法是阻塞方法,也就是說調用accept方法後程序會停下來等待連接請求,在接受請求之前程序將不會繼續執行,當接收到請求後accept方法返回一個Socket。
•使用accept方法返回的Socket與客戶端進行通信
如下代碼,我們在服務器端創建ServerSocket,並調用accept方法監聽Client的請求,收到請求後返回一個Socket。
public class Server {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//創建一個ServerSocket監聽8080端口
ServerSocket server = new ServerSocket(8080);
//等待請求
Socket socket = server.accept();
//接受請求後使用Socket進行通信,創建BufferedReader用於讀取數據
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = is.readLine();
System.out.println("received frome client:" + line);
//創建PrintWriter,用於發送數據
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("this data is from server");
pw.flush();
//關閉資源
pw.close();
is.close();
socket.close();
server.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
然後我們再看看客戶端的Socket代碼,Socket的使用也是一樣,首先創建一個Socket,Socket的構造方法非常多,這裡用的是Socket(String host, int port),把目標主機的地址和端口號傳入即可(本實驗代碼中服務器和Client代碼沒有在同一台機器上,服務器的IP地址:192.168.6.42,所以如果讀者在實驗過程中ServerSocket和Client在同一主機下,那麼Client中的IP地址需要更改為:127.0.0.1,Socket創建的過程就會跟服務器端建立連接,創建完Socket後,再創建Writer和Reader來傳輸數據,數據傳輸完成後釋放資源關閉連接。
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
String msg = "Client data";
try {
//創建一個Socket,跟服務器的8080端口鏈接
Socket socket = new Socket("192.168.6.42",8080);
//使用PrintWriter和BufferedReader進行讀寫數據
PrintWriter pw = new PrintWriter(socket.getOutputStream());
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//發送數據
pw.println(msg);
pw.flush();
//接收數據
String line = is.readLine();
System.out.println("received from server" + line);
//關閉資源
pw.close();
is.close();
socket.close();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
最後先啟動Server然後啟動Client就可以完成一次Client和Server的通信。
NioSocket的用法
從JDK1.4開始,Java增加了新的IO模式-nio(new IO),nio在底層采用了新的處理方式,極大提高了IO的效率。我們使用的Socket也是IO的一種,nio提供了相應的工具:ServerSocketChannel和SocketChannel,他們分別對應原來的ServerSocket和Server。
在了解NioSocket之前我們先了解Buffer、Channel、Selector。為了方便理解,我們來看個例子,要過聖誕節了,需要給同學們發賀卡和蘋果,班長這時候又是最辛苦的,每次拿一個蘋果和一張賀卡發給一個同學,發送完成後回來再取一張賀卡和一個蘋果發給另一個同學,直到全班同學都拿到賀卡和蘋果為止,這就是普通Socket處理方式,來一個請求,ServerSocket就進行處理,處理完成後繼續接受請求,這種方式效率很低啊!還是聖誕節的例子,班長發現班委不止他一個,就通知了生活委員(女)和組織委員(男)來幫助他發賀卡和蘋果,女生的賀卡是粉色的,男生的賀卡是藍色的,生活委員負責從全班的賀卡中挑選女生的賀卡,而組織委員則負責男生的賀卡,然後生活委員和組織委員分別以宿捨為單位通知宿捨長來領取宿捨同學的賀卡和蘋果,班長將聖誕節發蘋果和賀卡的工作布置給兩個班委後,就可以繼續干其他工作了。這就是NioSocket,Buffer就是所有傳遞的貨物,也就是例子中的蘋果和賀卡,而Channel就是傳遞貨物的通道,也就是例子中的宿捨長,負責將禮物搬回自己宿捨,而生活委員和組織委員充當了Selector的職責,負責禮物的分揀。
ServerSocketChannel可以使用自己的靜態工廠方法open創建,每個ServerSocketChannel對應一個ServerSocket(通過調用其socket()獲取),如果直接使用獲取的ServerSocket來監聽請求,那麼還是普通ServerSocket,而通過將獲取的ServerSocket綁定端口號來實現NioSocket。ServerSocketChannel可以通過configureBlocking方法來設置是否采用阻塞模式,如果設置為非阻塞模式,就可以調用register方法注冊Selector來使用了。
Selector可以通過其靜態工廠方法open創建,創建後通過Channel的register方法注冊到ServerSocketChannel或者SocketChannel上,注冊完成後Selector就可以通過select方法來等待請求,select方法有一個long類型參數,代表最長等待時間,如果在這段時間內收到相應操作的請求則返回可以處理的請求的數量,否則在超時後返回0,如果傳入的參數為0或者無參數的重載方法,select方法會采用阻塞模式知道有相應操作請求的出現。當接收到請求後Selector調用selectdKeys方法返回SelectionKey集合。
SelectionKey保存了處理當前請求的Channel和Selector,並且提供了不同的操作類型。Channel在注冊Selector時可以通過register的第二個參數選擇特定的操作(請求操作、連接操作、讀操作、寫操作),只有在register中注冊了相應的操作Selector才會關心相應類型操作的請求。
介紹了這麼多估計大家也煩了,我們就來看看服務器端NioSocket的處理過程吧:
1.創建ServerSocketChannel並設置相應的端口號、是否為阻塞模式
2.創建Selector並注冊到ServerSocketChannel上
3.調用Selector的selector方法等待請求
4.Selector接收到請求後使用selectdKeys返回SelectionKey集合
5.使用SelectionKey獲取到channel、selector和操作類型並進行具體操作。
public class NIOServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//創建ServerSocketChannel,監聽8080端口
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
//設置為非阻塞模式
ssc.configureBlocking(false);
//為ssc注冊選擇器
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
//創建處理器
Handler handler = new Handler(1024);
while(true){
//等待請求,每次等待阻塞3s,超過3s後線程繼續向下運行,如果傳入0或者不傳入參數則一直阻塞
if(selector.select(3000) == 0){
System.out.println("等待請求超時----");
continue;
}
System.out.println("處理請求----");
//獲取處理的SelectionKey
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key = keyIter.next();
try{
//接收到連接請求時
if(key.isAcceptable()){
handler.handleAccept(key);
}
//讀數據
if(key.isReadable()){
handler.handleRead(key);
}
}catch(IOException ex){
keyIter.remove();
continue;
}
//處理完後,從待處理的SelectionKey迭代器中移除當前所使用的key
keyIter.remove();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static class Handler{
private int bufferSize = 1024;
private String localCharset = "UTF-8";
public Handler(int bufferSize){
this.bufferSize = bufferSize;
}
public void handleAccept(SelectionKey key) throws IOException{
SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
public void handleRead(SelectionKey key) throws IOException{
//獲取Channel
SocketChannel sc = (SocketChannel) key.channel();
//獲取buffer並重置
ByteBuffer buffer = (ByteBuffer)key.attachment();
buffer.clear();
//沒有讀到內容則關閉
if(sc.read(buffer) == -1)
sc.close();
else{
//將buffer轉換為讀狀態
buffer.flip();
//將buffer中接收到的值按localCharset格式編碼後保存到receivedString
String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
System.out.println("received from client:" + receivedString);
//返回數據給客戶端
String sendString = "this data is from Server";
buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
sc.write(buffer);
sc.close();
}
}
}
}
客戶端代碼通普通Socket一樣,Socket socket = new Socket("192.168.6.42",8080);表示與服務器端建立連接,從而執行服務器端的handleAccept()方法,給ServerSocketChannel注冊selector以及添加SelectionKey.OP_READ參數,表示selector關心讀方法。然後通過PrintWrite在客戶端將內容發送給服務器端,服務器端執行handleRead方法對接收到的內容進行處理,並將結果返回給客戶端,客戶端通過BufferedReader接受數據,最後關閉連接。