更新時間:2022-07-20 06:29:12 來源:動力節點 瀏覽1720次
TCP(Transmission Control Protocol),即傳輸控制協議。是一種面向連接的、可靠的、基于字節流的傳輸層通信協議。不同于UDP,TCP更像是提供一種可靠的、像管道一樣的連接。
Java中的TCP主要涉及ServerSocket和Socket兩個類。前者被認為是服務端的一個實體,用于接受連接。后者則被認為是連接的一種封裝,用于傳輸數據,類似于一個管道。
下面就來實現一下服務端與客戶端。
服務端:
public class TCPService {
public static final String SERVICE_IP = "127.0.0.1";
public static final int SERVICE_PORT = 10101;
public static final char END_CHAR = '#';
public static void main(String[] args) {
TCPService service = new TCPService();
//啟動服務端
service.startService(SERVICE_IP,SERVICE_PORT);
}
private void startService(String serverIP, int serverPort){
try {
//封裝服務端地址
InetAddress serverAddress = InetAddress.getByName(serverIP);
//建立服務端
try(ServerSocket service = new ServerSocket(serverPort, 10, serverAddress)){
while (true) {
StringBuilder receiveMsg = new StringBuilder();
//接受一個連接,該方法會阻塞程序,直到一個鏈接到來
try(Socket connect = service.accept()){
//獲得輸入流
InputStream in = connect.getInputStream();
//解析輸入流,遇到終止符結束,該輸入流來自客戶端
for (int c = in.read(); c != END_CHAR; c = in.read()) {
if(c ==-1)
break;
receiveMsg.append((char)c);
}
//組建響應信息
String response = "Hello world " + receiveMsg.toString() + END_CHAR;
//獲取輸入流,并通過向輸出流寫數據的方式發送響應
OutputStream out = connect.getOutputStream();
out.write(response.getBytes());
}catch (Exception e){
e.printStackTrace();
}
}
}catch (Exception e){
e.printStackTrace();
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
客戶端
public class TCPClient {
public static void main(String[] args) {
TCPClient client = new TCPClient();
SimpleDateFormat format = new SimpleDateFormat("hh-MM-ss");
Scanner scanner = new Scanner(System.in);
while(true){
String msg = scanner.nextLine();
if("#".equals(msg))
break;
//打印響應的數據
System.out.println("send time : " + format.format(new Date()));
System.out.println(client.sendAndReceive(TCPService.SERVICE_IP,TCPService.SERVICE_PORT,msg));
System.out.println("receive time : " + format.format(new Date()));
}
}
private String sendAndReceive(String ip, int port, String msg){
//這里比較重要,需要給請求信息添加終止符,否則服務端會在解析數據時,一直等待
msg = msg+TCPService.END_CHAR;
StringBuilder receiveMsg = new StringBuilder();
//開啟一個鏈接,需要指定地址和端口
try (Socket client = new Socket(ip, port)){
//向輸出流中寫入數據,傳向服務端
OutputStream out = client.getOutputStream();
out.write(msg.getBytes());
//從輸入流中解析數據,輸入流來自服務端的響應
InputStream in = client.getInputStream();
for (int c = in.read(); c != TCPService.END_CHAR; c = in.read()) {
if(c==-1)
break;
receiveMsg.append((char)c);
}
}catch (Exception e){
e.printStackTrace();
}
return receiveMsg.toString();
}
}
單從代碼結構的角度來看,UDP通信服務端與客戶端代碼是相似的,都是依托于DatagramPacket 對象收發信息。而TCP通信中,只有服務端有一個實體,客戶端只要借助Socket收發信息即可,發送完關閉Socket。
上面有一點需要注意,在讀輸入流時,必須做讀到流結束判斷,就是讀到-1,若沒有做判斷,在這樣情況下會出錯:若一個連接連接成功后,沒有發生任何信息,或信息中沒有結束字符,就關閉了連接,由于TCP連接是雙向的,導致另一端一直從輸入流中讀到流結束標志,很快會導致OOM,所以在讀到結束符時,要及時跳出循環。結束符只會在連接中斷時發出,而在等待輸入時,不會出現,所以不必擔心在等待響應時由于讀到該字符導致服務端或客戶端提前中斷連接。
另外Socket和ServerSocket在jdk 1.7之后都實現了AutoCloseable接口,所以可以用try-with-resources結構。之前的UDP里的DatagramPacket 也一樣
這就是一個簡單的阻塞型服務器模型,分析代碼我們可知,如果一次請求時間過長,會影響到后續請求的執行。我們可以在服務端輸出時加一個sleep,啟動兩個客戶端,分別發送消息,觀察log,服務端延遲5s,結果如下:
客戶端1:
send time : 06-04-06
Hello world 1
receive time : 06-04-11
客戶端2:
send time : 06-04-08
Hello world 2
receive time : 06-04-16
其中客戶端1先發送,客戶端2后發送,可見客戶端在等待服務器處理完客戶端1的請求后才處理客戶端2的請求
由此我們可以預見,當服務器接到一個需要長時間處理的請求時,會阻塞后續的請求,這也就是這種類型服務器容易遭到攻擊的原因。為了應對這種局面,我們可以在收到一個請求時,調用子線程去處理,服務器時刻處在接受請求的狀態。
public class TCPService1 {
public static final String SERVICE_IP = "127.0.0.1";
public static final int SERVICE_PORT = 10101;
public static final char END_CHAR = '#';
public static void main(String[] args) {
TCPService1 service1 = new TCPService1();
service1.startService();
}
private void startService(){
try {
InetAddress address = InetAddress.getByName(SERVICE_IP);
Socket connect = null;
ExecutorService pool = Executors.newFixedThreadPool(5);
try (ServerSocket service = new ServerSocket(SERVICE_PORT,5,address)){
while(true){
connect = service.accept();
//創建一個任務
ServiceTask serviceTask = new ServiceTask(connect);
//放入線程池等待運行
pool.execute(serviceTask);
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(connect!=null)
connect.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
class ServiceTask implements Runnable{
private Socket socket;
ServiceTask(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
StringBuilder receiveMsg = new StringBuilder();
InputStream in = socket.getInputStream();
for (int c = in.read(); c != END_CHAR; c = in.read()) {
if(c ==-1)
break;
receiveMsg.append((char)c);
}
String response = "Hello world " + receiveMsg.toString() + END_CHAR;
Thread.currentThread().sleep(5000);
OutputStream out = socket.getOutputStream();
out.write(response.getBytes());
}catch (Exception e){
e.printStackTrace();
}finally {
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
在這個服務器中,我們采用了線程池的做法,每到一個請求,我們就向線程池中添加一個任務。實際運行情況如下:
客戶端1
send time : 03-04-59
Hello world 1
receive time : 03-04-04
客戶端2
send time : 03-04-01
Hello world 2
receive time : 03-04-06
可見每個客戶端能在發送信息后得到響應,不必排隊。但是這種類型的服務器并不能保證實時響應,當請求數過多時,服務器資源會被耗盡,或者服務器有最大線程數有限制,多余的請求依然會被阻塞。
第一二種服務器模型中,我們在讀取流的時候加入了自定義的結束符,同時采用Java for循環,但是一次從輸入流中讀一個數據,效率比較低,我們可以采用緩沖區的方法,但是這種方法不能判斷自定義的結束符,只能判斷流結束,所以要及時關閉流,如客戶端發完數據后關閉輸出流:
OutputStream out = client.getOutputStream();
out.write(msg.getBytes());
client.shutdownOutput();
InputStream in = client.getInputStream();
int len;
byte[] buffer = new byte[1024];
while((len = in.read(buffer))!=-1)
receiveMsg.append(new String(buffer,0,len));
由于TCP通信是雙向的,所以可以單獨關閉一端,但是不能直接關閉輸入或輸出流,這樣會將整個Socket關閉。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習