0%

Java|Socket编程指南

That is a good excuse

概述

Socket编程是指编写在多台计算机上执行的程序,其中的设备都使用网络相互连接

Socket常用的通信协议有UDP和TCP,本文主要介绍通过TCP/IP网络协议进行Socket编程

Socket通信流程

img

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭;

服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

简单示例

Socket是网络上不同计算机运行的两个程序之间双向通信链路的一个端点。Socket需要绑定端口号,一遍传输层可以标识数据要发送到的应用程序

服务端

服务端会用到两个socket,一个叫作监听 socket,一个叫作已完成连接 socket

目前的服务器不能保证通信的连续性,它会在发送完消息后关闭连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

// socket服务端
public class Server {
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public void start(int port){
try{
// (监听socket)
// 绑定指定端口,使服务器的Socket在指定端口号上运行
serverSocket = new ServerSocket(port);

// (已连接socket)
// 服务器遇到accept进入阻塞,等待客户端发出连接
// 连接成功后,服务器将获得绑定到同一本地端口6666的新socket,用于传输数据
clientSocket = serverSocket.accept();

// 输出流,可发送消息到客户端
out = new PrintWriter(clientSocket.getOutputStream(), true);
// 输入流,可接收客户端消息
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

String greeting = in.readLine();

if(greeting.equals("hello server")){
out.println("hello client");
}
else{
out.println("unrecognised greeting");
}

}
catch (IOException e){
e.printStackTrace();
}


}

public void stop(){
try{
in.close();
out.close();
clientSocket.close();
serverSocket.close();

}
catch (IOException e){
e.printStackTrace();
}


}

public static void main(String[] args) {
Server server = new Server();
// 开启服务器
server.start(6666);
}

}

客户端

客户端只需要创建一个socket以保持连接,最终客户端的输入流连接到服务端的输出流,服务器的输入流连接到客户端的输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import com.sun.javafx.iio.ios.IosDescriptor;

import java.io.*;
import java.net.Socket;

public class Client {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public void startConnection(String ip, int port){

try{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);

// 获取socket的输入输出流,以便与服务端通信
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
catch (IOException e){
e.printStackTrace();
}



}

public String sendMessage(String msg){

// 客户端输出请求消息
out.println(msg);
String resp = null;
try {
// 客户端接收响应消息
resp = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return resp;

}

public void stopConnection(){

try {
in.close();
out.close();
clientSocket.close();
}
catch (IOException e){
e.printStackTrace();
}


}

}

测试

先手动启动服务端,然后运行以上测试案例即可完成一次连接和一次消息发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.junit.Test;
public class HelloTest {
@Test
public void hello(){
Client client = new Client();
// 客户端对服务端发起连接
client.startConnection("127.0.0.1", 6666);
// 客户端发送消息到服务端并接收响应结果
String response = client.sendMessage("hello server");
System.out.println(response);

}

}

若启动服务端时出现以下报错,是出现了端口占用,可以修改端口也可以关闭占用端口的进程

image-20220225125717019

Windows下使用命令行关闭占用端口的进程

1
2
3
4
5
6
7
8
// 参看端口号含6666的条目
netstat -ano|findstr "6666"

// 根据pid查询对应的应用程序
tasklist|findstr "1828"

// 杀死进程
taskkill /f /pid 1828

持续连接优化

在前一个案例中,服务器会阻塞直到客户端连接它。在单个消息后,连接就会关闭,客户端和服务端无法持续沟通,因此仅仅会出现在ping请求中

如果要实现一个聊天服务器,客户端和服务端之间就需要连续的来回通信

服务端

在优化中我在服务端创建一个while循环来连续观察传来消息的服务器输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public void start(int port){
try{
// (监听socket)
// 绑定指定端口,使服务器的Socket在指定端口号上运行
serverSocket = new ServerSocket(port);

// (已连接socket)
// 服务器遇到accept进入阻塞,等待客户端发出连接
// 连接成功后,服务器将获得绑定到同一本地端口4444的新socket,用于传输数据
clientSocket = serverSocket.accept();

// 输出流,可发送消息到客户端
out = new PrintWriter(clientSocket.getOutputStream(), true);
// 输入流,可接收客户端消息
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

String inputLine;
// while循环连续观察从客户端传来消息的服务器输入流
// 直到读取到exit断开连接
while ((inputLine = in.readLine()) != null){
if(inputLine.equals("exit")){
out.println("goodbye");
break;
}
out.println(inputLine.replace("req", "res"));

}
}
catch (IOException e){
e.printStackTrace();
}
}


public void stop(){
try{
in.close();
out.close();
clientSocket.close();
serverSocket.close();

}
catch (IOException e){
e.printStackTrace();
}


}

public static void main(String[] args) {
EchoServer server = new EchoServer();
// 开启服务器
server.start(4444);
}

}

客户端

客户端不需要进行优化修改,这里为了方区分创建一个新的类EchoClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.io.*;
import java.net.Socket;

public class EchoClient {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public void startConnection(String ip, int port){

try{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);

// 获取socket的输入输出流,以便与服务端通信
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
catch (IOException e){
e.printStackTrace();
}



}


public String sendMessage(String msg){

// 客户端输出请求消息
out.println(msg);
String resp = null;
try {
// 客户端接收响应消息
resp = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return resp;

}


public void stopConnection(){

try {
in.close();
out.close();
clientSocket.close();
}
catch (IOException e){
e.printStackTrace();
}


}

}

测试

在初始示例中,我们只在服务器关闭连接之前进行一次通信。现在,我们发送一个终止信号,以便在会话完成时告诉服务器,以此关闭服务器的socket进程

开启EchoServer服务器运行以下测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class EchoTest {

private EchoClient client = new EchoClient();

@Before
public void setup(){
client.startConnection("127.0.0.1", 4444);
}

@After
public void tearDown(){
client.stopConnection();
}

@Test
public void echo(){
String resp1 = client.sendMessage("req:hello");
String resp2 = client.sendMessage("req:world");
String resp3 = client.sendMessage("exit");

System.out.println(resp1);
System.out.println(resp2);
System.out.println(resp3);
}

}

  • @BeforeClass – 表示在类中的任意public static void方法执行之前执行
  • @AfterClass – 表示在类中的任意public static void方法执行之后执行
  • @Before – 表示在任意使用@Test注解标注的public void方法执行之前执行
  • @After – 表示在任意使用@Test注解标注的public void方法执行之后执行
  • @Test – 使用该注解标注的public void方法会表示为一个测试方法

多客户端优化

在实际情况中,服务端常常要处理多个客户端的请求,为此我们要在服务端为每一个客户端请求创建一个新的socket线程,即提供服务的客户端数将等于服务端正在运行的线程数

服务端

仍然用一个监听socket在主线程监听端口,而需要多线程存储已连接socket以保持与多个客户端的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoMultiSever {
private ServerSocket serverSocket;

public void start(int port){

try {
// 仍然使用一个socket在主线程中监听端口
serverSocket = new ServerSocket(port);
while (true){
// 每次循环中,accept会阻塞调用,直到新的客户端调用
// 连接成功后,EchoMultiServer会将已连接的socket委托给 EchoClientHandler
new EchoClientHandler(serverSocket.accept()).start();
}
}
catch (IOException e){
e.printStackTrace();
}

}

public void stop(){
try{
serverSocket.close();
}
catch (IOException e){
e.printStackTrace();
}
}

// 创建一个单独的线程EchoClientHandler
// 保存已连接的socket与客户端交流
public static class EchoClientHandler extends Thread{
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public EchoClientHandler(Socket socket){
this.clientSocket = socket;
}

// 线程执行start直到运行run方法,与目标客户端进行交流
// 其内部发生的情况与EchoSever相同
public void run(){

try{

out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

String inputLine;
while ((inputLine = in.readLine()) != null){
if(inputLine.equals("exit")){
out.println("bye");
break;
}
out.println(inputLine.replace("req", "res"));
}

in.close();
out.close();
clientSocket.close();

}
catch (IOException e){
e.printStackTrace();
}


}


}

public static void main(String[] args) {
EchoMultiSever server = new EchoMultiSever();
// 开启服务器
server.start(5555);
}


}

客户端

客户端不需要进行优化修改,与上面的相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.io.*;
import java.net.Socket;

public class EchoMultiClient {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;

public void startConnection(String ip, int port){

try{
// 客户端需要知道服务端的ip和其正在监听的端口号,才能发起连接
// 服务器接收连接后创建客户端socket
clientSocket = new Socket(ip, port);

// 获取socket的输入输出流,以便与服务端通信
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
catch (IOException e){
e.printStackTrace();
}



}

public String sendMessage(String msg){

out.println(msg);
String resp = null;
try {
resp = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return resp;

}

public void stopConnection(){

try {
in.close();
out.close();
clientSocket.close();
}
catch (IOException e){
e.printStackTrace();
}


}

}

测试

测试类中需要发起多个客户端请求

运行EchoMultiSever后,运行以下案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.junit.Test;

// 测试方法开启多个客户端请求
public class EchoMultiTest {
@Test
public void buildClient1(){
EchoClient client1 = new EchoClient();
client1.startConnection("127.0.0.1", 5555);
String resp1 = client1.sendMessage("req:hello");
String resp2 = client1.sendMessage("req:world");
String resp3 = client1.sendMessage("exit");

System.out.println(resp1);
System.out.println(resp2);
System.out.println(resp3);
}

@Test
public void buildClient2(){
EchoClient client2 = new EchoClient();
client2.startConnection("127.0.0.1", 5555);
String resp1 = client2.sendMessage("req:hello");
String resp2 = client2.sendMessage("req:world");
String resp3 = client2.sendMessage("exit");

System.out.println(resp1);
System.out.println(resp2);
System.out.println(resp3);
}

@Test
public void buildClient3(){
EchoClient client3 = new EchoClient();
client3.startConnection("127.0.0.1", 5555);
String resp1 = client3.sendMessage("req:hello");
String resp2 = client3.sendMessage("req:world");
String resp3 = client3.sendMessage("exit");

System.out.println(resp1);
System.out.println(resp2);
System.out.println(resp3);
}

}

参考文章

Java 套接字

TCP/IP图解