一個客戶端對一個服務端
- 客戶端與服務端一直保持socket連接通過控制檯循環交互
- 具體表現爲客戶端發起請求,服務端接受客戶端請請求並在控制檯輸入響應, 客戶端接受服務端響應, 循環進行以上步驟
服務端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* @Description TODO socket客戶端
* @Author JianPeng OuYang
* @Date 2020/1/21 18:12
* @Version v1.0
*/
// 1. 服務端:創建ServerSocket對象,綁定監聽端口
// 2. 服務端:通過accept()方法監聽客戶端請求
// 3. 客戶端:創建Socket對象,指明需要連接的服務器的地址和端口號
// 4. 客戶端:連接建立後,通過輸出流向服務器發送請求信息
// 5. 服務端:連接建立後,通過輸入流讀取客戶端發送的請求信息
// 6. 服務端:通過輸出流向客戶端發送響應信息
// 7. 客戶端:通過輸入流獲取服務器相應的信息
//
// - 客戶端、服務器端都使用Socket中的getInputStream方法和getOutputStream方法獲得輸入流和輸出流,進一步進行數據讀寫操作
public class SocketClient {
private static String HOST = "127.0.0.1";
private static int PROT = 8080;
private static String CHARSET = "utf-8";
public static void main(String[] args) {
BufferedWriter bw = null;
BufferedReader br = null;
try {
Socket clientSocket = new Socket(HOST, PROT);
System.out.println("客戶端初始化。。。。。");
// 獲取連接服務端的輸入輸出流,用於向服務器提交數據或者獲取響應
bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));
//放在這裏表示5秒內客戶端與服務端已經建立連接,但5秒內沒有讀取到服務端響應,則會拋出異常
//clientSocket.setSoTimeout(5000);
// 在一次連接中循環與服務端進行交互
while (true) {
//客戶端發起請求
System.out.print("客戶端發起請求=>");
Scanner scanner = new Scanner(System.in);
String request = scanner.nextLine();
bw.write(request);
//因爲在服務端使用的是readLine,所以如果不調用newLine,那麼會一直阻塞
bw.newLine();
bw.flush();
//clientSocket.shutdownOutput();
String reqponse = br.readLine();// read()和readLine()都會讀取對端發送過來的數據,如果無數據可讀,就會阻塞直到有數據可讀。或者到達流的末尾,這個時候分別返回-1和null。
System.out.println("客戶端接受響應=>"+reqponse);
if(request.equals("exit")){
System.out.println("客戶端關閉連接");
clientSocket.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//這裏不能隨便關閉流,否則會把socket也關閉了(因爲後面還要發送數據,所以不能關閉流,不管是關閉輸入輸入其中之一,都會導致輸入和輸出都不能使用)
// 我這裏是因爲使用了循環,不手動結束循環不會走finally
try {
if (bw != null) {
bw.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客戶端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* @Description TODO socket客戶端
* @Author JianPeng OuYang
* @Date 2020/1/21 18:12
* @Version v1.0
*/
// 1. 服務端:創建ServerSocket對象,綁定監聽端口
// 2. 服務端:通過accept()方法監聽客戶端請求
// 3. 客戶端:創建Socket對象,指明需要連接的服務器的地址和端口號
// 4. 客戶端:連接建立後,通過輸出流向服務器發送請求信息
// 5. 服務端:連接建立後,通過輸入流讀取客戶端發送的請求信息
// 6. 服務端:通過輸出流向客戶端發送響應信息
// 7. 客戶端:通過輸入流獲取服務器相應的信息
//
// - 客戶端、服務器端都使用Socket中的getInputStream方法和getOutputStream方法獲得輸入流和輸出流,進一步進行數據讀寫操作
public class SocketClient {
private static String HOST = "127.0.0.1";
private static int PROT = 8080;
private static String CHARSET = "utf-8";
public static void main(String[] args) {
BufferedWriter bw = null;
BufferedReader br = null;
try {
Socket clientSocket = new Socket(HOST, PROT);
System.out.println("客戶端初始化。。。。。");
// 獲取連接服務端的輸入輸出流,用於向服務器提交數據或者獲取響應
bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), CHARSET));
br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), CHARSET));
//放在這裏表示5秒內客戶端與服務端已經建立連接,但5秒內沒有讀取到服務端響應,則會拋出異常
//clientSocket.setSoTimeout(5000);
// 在一次連接中循環與服務端進行交互
while (true) {
//客戶端發起請求
System.out.print("客戶端發起請求=>");
Scanner scanner = new Scanner(System.in);
String request = scanner.nextLine();
bw.write(request);
//因爲在服務端使用的是readLine,所以如果不調用newLine,那麼會一直阻塞
bw.newLine();
bw.flush();
//clientSocket.shutdownOutput();
String reqponse = br.readLine();// read()和readLine()都會讀取對端發送過來的數據,如果無數據可讀,就會阻塞直到有數據可讀。或者到達流的末尾,這個時候分別返回-1和null。
System.out.println("客戶端接受響應=>"+reqponse);
if(request.equals("exit")){
System.out.println("客戶端關閉連接");
clientSocket.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//這裏不能隨便關閉流,否則會把socket也關閉了(因爲後面還要發送數據,所以不能關閉流,不管是關閉輸入輸入其中之一,都會導致輸入和輸出都不能使用)
// 我這裏是因爲使用了循環,不手動結束循環不會走finally
try {
if (bw != null) {
bw.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服務端與客戶端初始化
客戶端發起請求
服務端接受請求並通過控制檯向客戶端輸出響應數據
客戶端與服務端的socket連接並沒有斷開,因此客戶端與服務端可以持續交互
一個服務器對多個客戶端
- 服務端每次連接成功一個客戶端,則啓動一個線程爲其服務
- 客戶端與服務端請求響應結束之後,分別斷開連接,具體表現爲“一次請求-一次響應”
服務端
- 可以看到上面的服務器端程序和客戶端程序是
一對一
的關係,爲了能讓一個服務器端程序能同時爲多個客戶提供服務,可以使用多線程機制,每個客戶端的請求都由一個獨立的線程進行處理。
下面是改寫後的服務器端程序。
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
//
/*在網絡中,我們可以利用ip地址+協議+端口號唯一標示網絡中的一個進程.而socket編程就是爲了完成兩個唯一進程之間的通信(一個是客戶端,一個是服務器端),其中用到的協議是TCP/UDP協議,它們都屬於傳輸層的協議.
TCP是基於連接的協議,在收發數據前,需要建立可靠的連接,也就是所謂的(三次握手).使用TCP協議時,數據會準確到達,但是效率較低.
UDP是面向非連接的協議,它不與對方建立連接,而是直接就把數據包發送過去.使用UDP協議時,傳輸效率高,但是不能保證數據準確到達,視頻聊天,語音聊天時就用的UDP協議.
以使用TCP協議通訊的socket爲例,其交互流程大概是這樣子的:
服務器端
創建服務器端的socket
綁定端口號
監聽端口
接收客戶端的連接請求
讀取客戶端發送數據
關閉socket
客戶端
創建客戶端的socket
連接服務器端的端口
向服務器端發送數據
關閉socket*/
/**
* @Description TODO 單一服務器對多客戶端 服務端:每次連接成功一個客戶端,則啓動一個線程爲其服務,一次請求一次響應
* @Author JianPeng OuYang
* @Date 2020/1/26 13:58
* @Version v1.0
*/
public class ServiceSocket {
private static int PROT = 8080;
private static String CHARSET = "UTF-8";
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);//創建綁定到特定端口的服務器Socket.
Socket socket = null;//需要接收的客戶端Socket
int count = 0;//記錄客戶端數量
System.out.println("服務器啓動");
//定義一個死循環,不停的接收客戶端連接
while (true) {
socket = serverSocket.accept();//偵聽並接受到此套接字的連接
InetAddress inetAddress=socket.getInetAddress();//獲取客戶端的連接
ServerThread thread=new ServerThread(socket,inetAddress);//自己創建的線程類
thread.start();//啓動線程
count++;//如果正確建立連接
System.out.println("客戶端數量:" + count);//打印客戶端數量
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
*處理客戶端請求的線程
*/
class ServerThread extends Thread {
Socket socket = null;
InetAddress inetAddress=null;//接收客戶端的連接
public ServerThread(Socket socket,InetAddress inetAddress) {
this.socket = socket;
this.inetAddress=inetAddress;
}
@Override
public void run() {
BufferedReader br = null;//爲輸入流添加緩衝
BufferedWriter bw = null;//爲輸出流添加緩衝
try {
br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String info = null;//臨時
//循環讀取客戶端信息
while ((info = br.readLine()) != null) {
//獲取客戶端的ip地址及發送數據
System.out.println("服務器端接收:"+"{'from_client':'"+socket.getInetAddress().getHostAddress()+"','data':'"+info+"'}");
}
socket.shutdownInput();//關閉輸入流
//響應客戶端請求
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
bw.write("{'to_client':'"+inetAddress.getHostAddress()+"','data':'我是服務器數據'}");
bw.newLine();
bw.flush();//清空緩衝區數據
} catch (IOException e) {
e.printStackTrace();
} finally {
//關閉資源
try {
CloseUtil.closeAll(bw,br);
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
accept()方法
-
注意到代碼accept()表示每當有新的客戶端連接進來後,就返回一個Socket實例,這個Socket實例就是用來和剛連接的客戶端進行通信的。由於客戶端很多,要實現併發處理,我們就必須爲每個新的Socket創建一個新線程來處理,這樣,主線程的作用就是接收新的連接,每當收到新連接後,就創建一個新線程進行處理。
-
如果沒有客戶端連接進來,accept()方法會阻塞並一直等待。如果有多個客戶端同時連接進來,ServerSocket會把連接扔到隊列裏,然後一個一個處理。對於Java程序而言,只需要通過循環不斷調用accept()就可以獲取新的連接。
-
這裏可以利用線程池來處理客戶端連接,能大大提高運行效率。
客戶端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* @Description TODO 單一服務器對多客戶端 服務端:每次連接成功一個客戶端,則啓動一個線程爲其服務
* @Author JianPeng OuYang
* @Date 2020/1/26 13:58
* @Version v1.0
*/
public class ClientSocket {
private static String HOST = "127.0.0.1";
private static int PROT = 8080;
private static String CHARSET = "utf-8";
public static void main(String[] args) {
try {
Socket socket = new Socket(HOST, PROT);
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), CHARSET));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), CHARSET));
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入數據:");
String data = scanner.nextLine();
bw.write(data);
bw.flush();//刷新緩衝
socket.shutdownOutput();//只關閉輸出流而不關閉連接
//獲取服務器端的響應數據
String info = null;
System.out.println("客戶端IP地址:" + socket.getInetAddress().getHostAddress());
//輸出服務器端響應數據
while ((info = br.readLine()) != null) {
System.out.println("客戶端接收:" + info);
}
CloseUtil.closeAll(br,bw);
//關閉資源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
關閉IO工具類
import java.io.Closeable;
import java.io.IOException;
/**
* @Description TODO
* @Author JianPeng OuYang
* @Date 2020/1/26 13:59
* @Version v1.0
*/
public class CloseUtil {
public static void closeAll(Closeable... arr) {
if (arr == null) {
return;
}
try {
for (Closeable cloneable : arr) {
cloneable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服務端初始化
客戶端初始化並與服務端建立連接
建立連接
客戶端發起請求並接收服務端響應後斷開與服務端連接,表示一次請求響應結束
服務端響應結束後等待其他連接
上面改進後的服務器端代碼可以支持不斷地併發響應網絡中的客戶請求。關鍵的地方在於多線程機制的運用,同時利用線程池可以改善服務器程序的性能。
一個服務器對多個客戶端的基於控制檯的聊天室
服務端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* @Description TODO socket服務端
* @Author JianPeng OuYang
* @Date 2020/1/21 18:12
* @Version v1.0
*/
// 1. 服務端:創建ServerSocket對象,綁定監聽端口
// 2. 服務端:通過accept()方法監聽客戶端請求
// 3. 客戶端:創建Socket對象,指明需要連接的服務器的地址和端口號
// 4. 客戶端:連接建立後,通過輸出流向服務器發送請求信息
// 5. 服務端:連接建立後,通過輸入流讀取客戶端發送的請求信息
// 6. 服務端:通過輸出流向客戶端發送響應信息
// 7. 客戶端:通過輸入流獲取服務器相應的信息
//
// - 客戶端、服務器端都使用Socket中的getInputStream方法和getOutputStream方法獲得輸入流和輸出流,進一步進行數據讀寫操作
public class SocketServer {
private static String CHARSET = "utf-8";
private List<MyChannel> channelList = new ArrayList<>();
public static void main(String[] args) {
new SocketServer().start();
}
public void start() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
//放在這裏表示5秒內沒有客戶端與服務端已經建立連接,則會拋出異常
//serverSocket.setSoTimeout(5000);
System.out.println("服務端初始化。。。。。");
// 監聽客戶端請求
Socket client = serverSocket.accept();
MyChannel channel = new MyChannel(client);
channelList.add(channel);//統一管理
new Thread(channel).start(); //一條道路
//放在這裏表示客戶端與服務端已經建立連接,5秒內客戶端沒有發起請求,則會拋出異常
//socket.setSoTimeout(5000)
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 一個客戶端 一條道路
* 1、輸入流
* 2、輸出流
* 3、接收數據
* 4、發送數據
*/
private class MyChannel implements Runnable {
private BufferedReader br = null;
private BufferedWriter bw = null;
private boolean isRunning = true;
private String name = null;
public MyChannel(Socket client) {
//服務器端都使用Socket中的getInputStream方法和getOutputStream方法獲得輸入流和輸出流,進一步進行數據讀寫操作
try {
br = new BufferedReader(new InputStreamReader(client.getInputStream(), CHARSET));
bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), CHARSET));
this.name = br.readLine();
this.send("歡迎您進入聊天室");
sendOtherClients(this.name + "進入了聊天室", true);
} catch (IOException e) {
isRunning = false;
CloseUtil.closeAll(bw, br);
}
}
/**
* 發送數據
*
* @param msg
*/
private void send(String msg) {
try {
bw.write(msg);
bw.newLine();
bw.flush();
} catch (IOException e) {
isRunning = false;
CloseUtil.closeAll(bw, br);
channelList.remove(this);//移除自身
}
}
/**
* 接收數據
*
* @return
*/
private String receive() {
String msg = "";
try {
msg = br.readLine();
return msg;
} catch (IOException e) {
isRunning = false;
CloseUtil.closeAll(bw, br);
channelList.remove(this);//移除自身
}
return msg;
}
/**
* 發送給其他客戶端
*/
private void sendOtherClients(String msg, boolean sys) {
if (msg.startsWith("@") && msg.indexOf(":") != -1) {
//獲取name
String name = msg.substring(1, msg.indexOf(":"));
String content = msg.substring(msg.indexOf(":") + 1);
for (MyChannel other : channelList) {
if (other.name.equals(name)) {
other.send(this.name + "對您悄悄地說:" + content);
}
}
} else {
//遍歷容器
for (MyChannel other : channelList) {
if (other == this) {
continue;
}
if (sys) { //系統信息
other.send("系統信息:" + msg);
} else {
//發送其他客戶端
other.send(this.name + "對所有人說:" + msg);
}
}
}
}
@Override
public void run() {
while (isRunning) {
sendOtherClients(receive(), false);
}
}
}
}
客戶端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* @Description TODO socket客戶端
* @Author JianPeng OuYang
* @Date 2020/1/21 18:12
* @Version v1.0
*/
// 1. 服務端:創建ServerSocket對象,綁定監聽端口
// 2. 服務端:通過accept()方法監聽客戶端請求
// 3. 客戶端:創建Socket對象,指明需要連接的服務器的地址和端口號
// 4. 客戶端:連接建立後,通過輸出流向服務器發送請求信息
// 5. 服務端:連接建立後,通過輸入流讀取客戶端發送的請求信息
// 6. 服務端:通過輸出流向客戶端發送響應信息
// 7. 客戶端:通過輸入流獲取服務器相應的信息
//
// - 客戶端、服務器端都使用Socket中的getInputStream方法和getOutputStream方法獲得輸入流和輸出流,進一步進行數據讀寫操作
public class SocketClient {
private static String HOST = "127.0.0.1";
private static int PROT = 8080;
public static void main(String[] args) {
try {
System.out.println("請輸入名稱:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String name = br.readLine();
if(name.equals("")){
return;
}
Socket clientSocket = new Socket(HOST, PROT);
System.out.println("客戶端初始化。。。。。");
new Thread(new Send(clientSocket,name)).start();
new Thread(new Receive(clientSocket)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端發送數據線程
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 發送線程
*
* @author Administrator
*/
public class Send implements Runnable {
private BufferedWriter bw = null;
private Boolean isRunning = true;
private String name;
public Send(Socket client, String name) {
try {
this.name = name;
this.bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8"));
send(name);
} catch (IOException e) {
CloseUtil.closeAll(bw);
isRunning = false;
}
}
private String getMsgFromConsole() {
Scanner scanner = new Scanner(System.in);
String request = scanner.nextLine();
return request;
}
public void send(String msg) {
try {
if (msg != null && !"".equals(msg)) {
bw.write(msg);
bw.newLine();
bw.flush();
System.out.println(Thread.currentThread().getName() + "=>send(" + msg + ")");
}
} catch (IOException e) {
isRunning = false;
CloseUtil.closeAll();
}
}
@Override
public void run() {
while (isRunning) {
send(getMsgFromConsole());
}
}
}
客戶端接受數據線程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 接收線程
* @author Administrator
*
*/
public class Receive implements Runnable {
private BufferedReader br = null;
private Boolean isRunning = true;
public Receive(Socket client) {
try {
this.br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
} catch (IOException e) {
CloseUtil.closeAll(br);
isRunning = false;
}
}
private String receive() {
try {
String response = br.readLine();
return response;
} catch (IOException e) {
CloseUtil.closeAll(br);
isRunning = false;
}
return null;
}
@Override
public void run() {
while (isRunning) {
System.out.println(Thread.currentThread().getName()+"=>receive():"+receive());
}
}
}
關閉IO工具類
import java.io.Closeable;
import java.io.IOException;
/**
* @Description TODO
* @Author JianPeng OuYang
* @Date 2020/1/26 13:59
* @Version v1.0
*/
public class CloseUtil {
public static void closeAll(Closeable... arr) {
if (arr == null) {
return;
}
try {
for (Closeable cloneable : arr) {
cloneable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服務端初始化,等待客戶端發起連接
客戶端初始化連接服務端,並創建張三用戶
客戶端初始化連接服務端,並創建李四用戶
張三非當前用戶發起聊天
李四對張三私發一條消息
Socket流
當Socket連接創建成功後,無論是服務器端,還是客戶端,我們都使用Socket實例進行網絡通信。因爲TCP是一種基於流的協議,
因此,Java標準庫使用InputStream和OutputStream來封裝Socket的數據流,這樣我們使用Socket的流,和普通IO流類似:
爲什麼寫入網絡數據時,要調用flush()方法。
- 如果不調用flush(),我們很可能會發現,客戶端和服務器都收不到數據,這並不是Java標準庫的設計問題,而是
我們以流的形式寫入數據的時候,並不是一寫入就立刻發送到網絡,而是先寫入內存緩衝區,直到緩衝區滿了以後,纔會一次性真正發送到網絡,這樣設計的目的是爲了提高傳輸效率。
如果緩衝區的數據很少,而我們又想強制把這些數據發送到網絡,就必須調用flush()強制把緩衝區數據發送出去。
小結
使用Java進行TCP編程時,需要使用Socket模型:
- 服務器端用ServerSocket監聽指定端口;
- 客戶端使用Socket(InetAddress, port)連接服務器;
- 服務器端用accept()接收連接並返回Socket;
- 雙方通過Socket打開InputStream/OutputStream讀寫數據;
- 服務器端通常使用多線程同時處理多個客戶端連接,利用線程池可大幅提升效率;
- flush()用於強制輸出緩衝區到網絡。