Markdown版本筆記 | 個人GitHub首頁 | 個人博客 | 個人微信 | 個人郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
TCP Socket 即時通信 API 示例html
public class SocketActivity extends ListActivity implements Client.MsgListener { public static final int PORT = 11232; public static final String HOST = "192.168.1.187"; //此 IP 爲內網 IP ,全部只有在同一局域網下才能通信(鏈接同一WIFI便可) private TextView msgTextView; private EditText editText; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] array = {"開啓服務器", "開啓客戶端", "客戶端發消息", "客戶端下線"}; setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, Arrays.asList(array))); editText = new EditText(this); getListView().addFooterView(editText); msgTextView = new TextView(this); getListView().addFooterView(msgTextView); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { switch (position) { case 0: Server.SINGLETON.startServer(PORT); break; case 1: Client.SINGLETON.startClient(HOST, PORT); Client.SINGLETON.setListener(this); break; case 2: Client.SINGLETON.sendMsg(editText.getText().toString()); break; case 3: Client.SINGLETON.exit(); break; default: break; } } private SpannableString getSpannableString(String string, int color) { SpannableString mSpannableString = new SpannableString(string); ForegroundColorSpan colorSpan = new ForegroundColorSpan(color); mSpannableString.setSpan(colorSpan, 0, mSpannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return mSpannableString; } @Override public void onReveiveMsg(String message) { runOnUiThread(() -> msgTextView.append(getSpannableString(message + "\n", Color.BLUE))); } @Override public void onSendMsg(String message) { runOnUiThread(() -> { String text = Client.SINGLETON.getName() + " : " + editText.getText().toString() + "\n"; msgTextView.append(getSpannableString(text, Color.RED)); }); } }
public enum Server { SINGLETON; public static final int MAX_TEXT_SIZE = 1024; public static final String CLIENT_EXIT_CMD = "拜拜"; public static final String CHARSET = "GBK"; private Set<Socket> socketSet; private boolean serverExit = false; private ServerSocket server;//服務器對象 Server() { socketSet = new HashSet<>();//用戶集合 } public void startServer(int port) { if (server != null && !server.isClosed()) { System.out.println("服務器已開啓,不須要重複開啓"); } else { new Thread(() -> { try { server = new ServerSocket(port); } catch (IOException e) { e.printStackTrace(); System.out.println("服務器啓動失敗"); return; } System.out.println("服務器啓動成功"); //循環監聽 while (!serverExit) { try { Socket socket = server.accept();//獲取鏈接過來的客戶端對象。阻塞方法 socketSet.add(socket); sendUserMsg(socket, getName(socket) + " 上線了, 當前 " + socketSet.size() + " 人在線"); listenerUserMsg(socket); //監聽客戶端發送的消息,並轉發給其餘用戶 } catch (IOException e) {//用戶下線 e.printStackTrace(); } } System.out.println("服務器已關閉"); }).start(); } } private void listenerUserMsg(Socket socket) { new Thread(() -> { try { byte[] bytes = new byte[MAX_TEXT_SIZE]; int count; boolean clinetExit = false; while (!serverExit && !clinetExit && !socket.isClosed() && (count = socket.getInputStream().read(bytes)) != -1) { String text = new String(bytes, 0, count, CHARSET); System.out.println("服務器已收到【" + getName(socket) + "】發送的信息【" + text + "】"); clinetExit = CLIENT_EXIT_CMD.equals(text); sendUserMsg(socket, getName(socket) + " : " + text); } } catch (IOException e) {//關閉與此用戶相關的流 e.printStackTrace(); System.out.println(getName(socket) + " 異常掉線"); } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } socketSet.remove(socket); sendUserMsg(socket, getName(socket) + " 下線了, 當前 " + socketSet.size() + " 人在線"); } } }).start(); } private void sendUserMsg(Socket socket, String text) { for (Socket otherSocket : socketSet) {//遍歷全部的在線用戶 if (otherSocket != null && !otherSocket.isClosed() && !otherSocket.equals(socket)) { try { OutputStream outputStream = otherSocket.getOutputStream();//獲取相應的輸出流,把信息發送過去 outputStream.write(text.getBytes(CHARSET)); outputStream.flush(); System.out.println("服務器已轉發信息【" + text + "】給【" + getName(otherSocket) + "】"); } catch (IOException e) { e.printStackTrace(); System.out.println(getName(socket) + " 異常"); } } } } private String getName(Socket socket) { return "用戶" + socket.getInetAddress().getHostAddress() + "_" + socket.getPort(); } }
public enum Client { SINGLETON; Client() { } public static final int MAX_TEXT_SIZE = 1024; public static final String CLIENT_EXIT_CMD = "拜拜"; public static final String CHARSET = "GBK"; private boolean exit = false; private Socket socket; private MsgListener listener; public void startClient(String host, int port) { if (socket != null && !socket.isClosed()) { System.out.println("客戶端已開啓,不須要重複開啓"); } else { new Thread(() -> { try { socket = new Socket(host, port);//建立客戶端對象 listenerUserMsg(); //監聽消息 System.out.println("客戶端已開啓成功"); } catch (IOException e) { e.printStackTrace(); System.out.println("客戶端已開啓失敗"); } }).start(); } } private void listenerUserMsg() { new Thread(() -> { try { byte[] bytes = new byte[MAX_TEXT_SIZE]; int count; while (!exit && socket != null && !socket.isClosed() && (count = socket.getInputStream().read(bytes)) != -1) { String text = new String(bytes, 0, count, CHARSET); System.out.println(getName() + " 收到信息【" + text + "】"); if (listener != null) { listener.onReveiveMsg(text); } } } catch (IOException e) { e.printStackTrace(); } }).start(); } public void sendMsg(String text) { new Thread(() -> { if (socket != null && !socket.isClosed()) { try { socket.getOutputStream().write(text.getBytes(CHARSET));//獲取socket流中的輸出流將指定的數據寫出去 if (listener != null) { listener.onSendMsg(text); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } public void exit() { new Thread(() -> { exit = true; if (socket != null && !socket.isClosed()) { try { socket.getOutputStream().write(CLIENT_EXIT_CMD.getBytes(CHARSET)); socket.close(); System.out.println("客戶端下線成功"); } catch (IOException e) { e.printStackTrace(); System.out.println("客戶端下線異常"); } } else { System.out.println("客戶端已下線,不須要重複下線"); } }).start(); } public String getName() { return "用戶" + socket.getInetAddress().getHostAddress() + "_" + socket.getPort(); } public void setListener(MsgListener listener) { this.listener = listener; } public interface MsgListener { void onReveiveMsg(String message); void onSendMsg(String message); } }
public class SocketActivity extends ListActivity implements Client.MsgListener { private boolean tag = true; public static final int PORT1 = 11233; public static final int PORT2 = 11234; public static final String HOST1 = "192.168.2.124"; public static final String HOST2 = "192.168.2.177"; private TextView msgTextView; private EditText editText; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] array = {"切換配置", "開啓客戶端", "客戶端發消息", "客戶端下線"}; setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, Arrays.asList(array))); editText = new EditText(this); getListView().addFooterView(editText); msgTextView = new TextView(this); getListView().addFooterView(msgTextView); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { switch (position) { case 0: tag = !tag; break; case 1: Client.SINGLETON.startClient(tag ? PORT1 : PORT2); Client.SINGLETON.setListener(this); break; case 2: try { //getLocalHost(),getByName("127.0.0.1"),getByName("192.168.0.255"),getByAddress(new byte[]{127,0,0,1}) InetAddress address = InetAddress.getByName(tag ? HOST2 : HOST1); Client.SINGLETON.sendMsg(editText.getText().toString(), address, tag ? PORT2 : PORT1); } catch (UnknownHostException e) { e.printStackTrace(); } break; case 3: Client.SINGLETON.exit(); break; default: break; } } private SpannableString getSpannableString(String string, int color) { SpannableString mSpannableString = new SpannableString(string); ForegroundColorSpan colorSpan = new ForegroundColorSpan(color); mSpannableString.setSpan(colorSpan, 0, mSpannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return mSpannableString; } @Override public void onReveiveMsg(String message) { runOnUiThread(() -> msgTextView.append(getSpannableString(message + "\n", Color.BLUE))); } @Override public void onSendMsg(String message) { runOnUiThread(() -> { String text = Client.SINGLETON.getName() + " : " + editText.getText().toString() + "\n"; msgTextView.append(getSpannableString(text, Color.RED)); }); } }
public enum Client { SINGLETON; Client() { } public static final int MAX_TEXT_SIZE = 1024; public static final String CHARSET = "GBK"; private boolean exit = false; private DatagramSocket send; private DatagramSocket receiver; private MsgListener listener; public void startClient(int port) { if (send != null && !send.isClosed()) { System.out.println("客戶端" + port + "已開啓,不須要重複開啓"); } else { new Thread(() -> { try { send = new DatagramSocket(); receiver = new DatagramSocket(port); listenerUserMsg(); //監聽消息 System.out.println("客戶端" + port + "已開啓成功"); } catch (IOException e) { e.printStackTrace(); System.out.println("客戶端" + port + "已開啓失敗"); } }).start(); } } private void listenerUserMsg() { new Thread(() -> { try { byte[] bytes = new byte[MAX_TEXT_SIZE]; DatagramPacket packet = new DatagramPacket(bytes, bytes.length); while (!exit && receiver != null && !receiver.isClosed()) { receiver.receive(packet);//持續接收數據 String text = new String(packet.getData(), 0, packet.getLength(), CHARSET); System.out.println(getName() + " 收到信息【" + text + "】"); if (listener != null) { listener.onReveiveMsg(text); } } } catch (IOException e) { e.printStackTrace(); } }).start(); } public void sendMsg(String text, InetAddress address, int port) { new Thread(() -> { if (send != null && !send.isClosed()) { try { String content = getName() + " : " + text; byte[] buf = content.getBytes(CHARSET); DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port); send.send(packet); if (listener != null) { listener.onSendMsg(text); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } public void exit() { new Thread(() -> { exit = true; if (send != null && !send.isClosed()) { send.close(); } if (receiver != null && !receiver.isClosed()) { receiver.close(); } System.out.println("客戶端下線成功"); }).start(); } public String getName() { return "用戶" + receiver.getLocalPort(); } public void setListener(MsgListener listener) { this.listener = listener; } public interface MsgListener { void onReveiveMsg(String message); void onSendMsg(String message); } }
BindException:Address already in use: JVM_Bind
該異常發生在服務器端進行new ServerSocket(port)
操做時。異常的緣由是此port端口已經被佔用。此時用netstat –an
命令,能夠看到一個Listending狀態的端口。只須要找一個沒有被佔用的端口就能解決這個問題。 ConnectException: Connection refused: connect
該異常發生在客戶端進行new Socket(ip, port)
操做時,該異常發生的緣由是或者具備此ip地址的機器不能找到,或者是該ip存在但找不到指定的端口進行監聽。出現該問題,首先檢查客戶端的ip和port是否寫錯了,若是正確則從客戶端ping
一下服務器看是否能ping通,若是能ping通再看在服務器端的監聽指定端口的程序是否啓動。 Socket is closed
該異常在客戶端和服務器都可能發生。異常的緣由是鏈接已被關閉後(調用了Socket的close方法)再對網絡鏈接進行讀寫操做。 SocketException:(Connection reset 或者 Connect reset by peer:Socket write error)
該異常在客戶端和服務器端均有可能發生,引發該異常的緣由有兩個,第一個就是若是一端的Socket被關閉,另外一端仍發送數據,發送的第一個數據包引起該異常(Connect reset by peer)。另外一個是一端退出,但退出時並未關閉該鏈接,另外一端若是在從鏈接中讀數據則拋出該異常(Connection reset)。簡單的說就是在鏈接斷開後的讀和寫操做引發的。 SocketException: Broken pipe
該異常在客戶端和服務器均有可能發生。在上面那種異常的第一種狀況中(也就是拋出SocketExcepton:Connect reset by peer:Socket write error),若是再繼續寫數據則拋出該異常。前兩個異常的解決方法是首先確保程序退出前關閉全部的網絡鏈接,其次是要檢測對方的關閉鏈接操做,發現對方關閉鏈接後本身也要關閉該鏈接。構造方法java
ServerSocket(int port)
建立綁定到特定端口的服務器套接字。經常使用方法android
Socket accept()
偵聽並接受到此套接字的鏈接。void close()
關閉此套接字。int getLocalPort()
返回此套接字在其上偵聽的端口。boolean isClosed()
返回 ServerSocket 的關閉狀態。構造方法git
Socket(InetAddress address, int port)
建立一個流套接字並將其鏈接到指定IP地址的指定端口號Socket(String host, int port)
建立一個流套接字並將其鏈接到指定主機上的指定端口號。經常使用方法github
void close()
關閉此套接字。InetAddress getInetAddress()
返回套接字鏈接的地址。InputStream getInputStream()
返回此套接字的輸入流。int getLocalPort()
返回此套接字綁定到的本地端口。OutputStream getOutputStream()
返回此套接字的輸出流。int getPort()
返回此套接字鏈接到的遠程端口。boolean isClosed()
返回套接字的關閉狀態。boolean isConnected()
返回套接字的鏈接狀態。構造方法web
DatagramSocket()
構造數據報套接字並將其綁定到本地主機上任何可用的端口。DatagramSocket(int port)
建立數據報套接字並將其綁定到本地主機上的指定端口。經常使用方法算法
void close()
關閉此數據報套接字。int getLocalPort()
返回此套接字綁定的本地主機上的端口號。int getPort()
返回此套接字的端口。boolean isClosed()
返回是否關閉了套接字。boolean isConnected()
返回套接字的鏈接狀態。void receive(DatagramPacket p)
今後套接字接收數據報包。void send(DatagramPacket p)
今後套接字發送數據報包。構造方法
接收瀏覽器
DatagramPacket(byte[] buf, int length)
構造 DatagramPacket,用來接收
長度爲 length 的數據包。接收
長度爲 length 的包,在緩衝區中指定了偏移量。發送到 InetAddress服務器
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
構造數據報包,用來將長度爲 length 的包 buf 發送
到指定主機address
上的指定端口號port
。發送
到指定主機上的指定端口號。發送到 SocketAddress微信
發送
到指定主機上的指定端口號。發送
到指定主機上的指定端口號。經常使用方法
get 方法
InetAddress getAddress()
返回某臺機器的 IP 地址,此數據報將要發往該機器或者是從該機器接收到的byte[] getData()
返回數據緩衝區。int getLength()
返回將要發送或接收到的數據的長度。int getOffset()
返回將要發送或接收到的數據的偏移量。int getPort()
返回某臺遠程主機的端口號,此數據報將要發往該主機或者是從該主機接收到的。set 方法
隨着互聯網的發展,傳統的HTTP協議已經很難知足Web應用日益複雜的需求了。近年來,隨着HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通訊
,擴展了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端發送數據
。
咱們知道,傳統的HTTP協議是無狀態
的,每次請求(request)都要由客戶端主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來講形成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通訊、實時數據、訂閱推送等功能的應用。在WebSocket規範提出以前,開發人員若要實現這些實時性較強的功能,常常會使用折衷的解決方法:輪詢(polling)和Comet技術
。其實後者本質上也是一種輪詢,只不過有所改進。
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求
,頻繁地查詢是否有新的數據改動。明顯地,這種方法會致使過多沒必要要的請求,浪費流量和服務器資源。
Comet技術又能夠分爲長輪詢和流技術
。長輪詢改進了上述的輪詢技術,減少了無用的請求。它會爲某些數據設定過時時間
,當數據過時後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的狀況。流技術一般是指客戶端使用一個隱藏的窗口與服務端創建一個HTTP長鏈接,服務端會不斷更新鏈接狀態以保持HTTP長鏈接存活
;這樣的話,服務端就能夠經過這條長鏈接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。
這兩種技術都是基於請求-應答
模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了必定流量在相同的頭部信息
上,而且開發複雜度也較大。
伴隨着HTML5推出的WebSocket,真正實現了Web的實時通訊,使B/S模式具有了C/S模式的實時通訊能力
。WebSocket的工做流程是這樣的:瀏覽器經過JavaScript向服務端發出創建WebSocket鏈接的請求,在WebSocket鏈接創建成功後,客戶端和服務端就能夠經過 TCP
鏈接傳輸數據。由於WebSocket鏈接本質上是TCP鏈接,不須要每次傳輸都帶上重複的頭部數據
,因此它的數據傳輸量比輪詢和Comet技術小 了不少。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。
JavaEE 7中出了JSR-356:Java API for WebSocket規範。很多Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat從7.0.27開始支持 WebSocket,從7.0.47開始支持JSR-356,下面的Demo代碼也是須要部署在Tomcat7.0.47以上的版本才能運行。
import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; /** * @ServerEndpoint 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端, * 註解的值將被用於監聽用戶鏈接的終端訪問URL地址,客戶端能夠經過這個URL來鏈接到WebSocket服務器端 */ @ServerEndpoint("/websocket") public class WebSocketTest { private static int onlineCount = 0; //當前在線鏈接數 //存放每一個客戶端對應的WebSocket對象 private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>(); private Session session;//與某個客戶端的鏈接會話,須要經過它來給客戶端發送數據 /** * 鏈接創建成功調用的方法 * @param session 可選的參數。session爲與某個客戶端的鏈接會話,須要經過它來給客戶端發送數據 */ @OnOpen public void onOpen(Session session){ this.session = session; webSocketSet.add(this); //加入set中 onlineCount++; //在線數加1 System.out.println("有新鏈接加入!當前在線人數爲" + getOnlineCount()); } /** * 鏈接關閉調用的方法 */ @OnClose public void onClose(){ webSocketSet.remove(this); //從set中刪除 onlineCount--; //在線數減1 System.out.println("有一鏈接關閉!當前在線人數爲" + getOnlineCount()); } /** * 收到客戶端消息後調用的方法 * @param message 客戶端發送過來的消息 * @param session 可選的參數 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("來自客戶端的消息:" + message); //羣發消息 for(WebSocketTest item: webSocketSet){ try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); continue; } } } /** * 發生錯誤時調用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error){ System.out.println("發生錯誤"); error.printStackTrace(); } /** * 這個方法與上面幾個方法不同。沒有用註解,是根據本身須要添加的方法。 * @param message * @throws IOException */ public void sendMessage(String message) throws IOException{ this.session.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void subOnlineCount() { WebSocketTest.onlineCount--; } }
public enum WebClient { SINGLETON; private static final String CHARSET = "UTF-8"; private WebSocketClient client = null; private MsgListener listener; public void startClient(URI serverUri) { if (client != null && !client.isClosed()) { System.out.println("客戶端" + getName() + "已開啓,不須要重複開啓"); } else { new Thread(() -> { client = new WebSocketClient(serverUri) { @Override public void onOpen(ServerHandshake handshakedata) { System.out.println("onOpen,握手"); Iterator<String> it = handshakedata.iterateHttpFields(); while (it.hasNext()) { String key = it.next(); System.out.println(key + ":" + handshakedata.getFieldValue(key)); } } @Override public void onMessage(String message) { System.out.println("onMessage,接收到消息【" + message + "】"); if (listener != null) { listener.onReveiveMsg(message); } } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("onClose,關閉:code=" + code + ",reason=" + reason + ",remote=" + remote); } @Override public void onError(Exception ex) { ex.printStackTrace(); } }; //client.connect(); //非阻塞方法 try { boolean success = client.connectBlocking();//阻塞方法 System.out.println("客戶端" + getName() + "鏈接" + (success ? "成功" : "失敗")); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("客戶端" + getName() + "鏈接異常"); } }).start(); } } public void sendMsg(String text) { new Thread(() -> { if (client != null && !client.isClosed()) { try { String content = getName() + " : " + text; client.send(content.getBytes(CHARSET)); if (listener != null) { listener.onSendMsg(text); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } public void exit() { new Thread(() -> { if (client != null && !client.isClosed()) { client.close(); } System.out.println("客戶端下線成功"); }).start(); } public String getName() { return "用戶" + client.getLocalSocketAddress().getPort(); } public void setListener(MsgListener listener) { this.listener = listener; } public interface MsgListener { void onReveiveMsg(String message); void onSendMsg(String message); } }
2018-11-23