Overview
socket编程旨在通过网络让运行再不同的电脑上的程序能相互通讯。
下面将介绍据于TPC/IP**的教程,使用Java编写客户端/服务器(C/S)应用程序。
小编这里有一份Java学习资料,加我的Java学习群:985331340免费获取。以下为部分资料截图
学这个教程前, 你应该已经知道:
- 基本的计算机网络
- Java IO的基本操作
- java 多线程的基本操作
- 单元测试
源代码请见github
简单例子
让我们弄清楚这些客户端和服务器最基本的双通(two-way communication)例子。
服务器
public class GreetServer {
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void start(int port) {
serverSocket = new ServerSocket(port);
clientSocket = serverSocket.accept();
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String greeting = in.readLine();
if ("hello server".equals(greeting)) {
out.println("hello client");
}
else {
out.println("无法识别问候");
}
}
public void stop() {
in.close();
out.close();
clientSocket.close();
serverSocket.close();
}
public static void main(String[] args) {
GreetServer server=new GreetServer();
server.start(6666);
}
}
客户端
public class GreetClient {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String ip, int port) {
clientSocket = new Socket(ip, port);
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
public String sendMessage(String msg) {
out.println(msg);
String resp = in.readLine();
return resp;
}
public void stopConnection() {
in.close();
out.close();
clientSocket.close();
}
}
启动服务器
我们可以使用单元测试来确认服务器向我们打招呼了。
@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
GreetClient client = new GreetClient();
client.startConnection("127.0.0.1", 6666);
String response = client.sendMessage("hello server");
assertEquals("hello client", response);
}
如果你完全不知道上面的代码是干啥的,不要担心,你要知道了还来看什么(滑稽,感觉我戏好多)。
接下来我们剖析这个例子, 来了解socket通讯(socket communication), 并通过其他例子来深入了解.
Socket是如何工作的
服务器
通常,服务器都运行在一台特定的电脑, 绑定了特定的端口(Port). 下面我们的服务器端口使用 6666.
ServerSocket serverSocket = new ServerSocket(6666);
这是一个拥塞算法, 服务器会一直等待,直到客户端的socket发出连接请求.
Socket clientSocket = serverSocket.accept();
服务器与客户端建立连接后, 服务器会建立一个新的socket, clientSocket
. 这个新的socket也会绑定相同的端口(6666) . 并且还将其远程端点设置为客户端的地址和端口(理解不了可以参考这个bilibili-憧憬少)。
此时, 此时,新的Socket对象直接连接了服务器与客户端. 然后, 我们可以通过这个新的socket来与客户端通讯.
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
从此处开始,服务器能够与客户端无休止地交换消息,直到socket将流关闭。
为了保证通信的连续性,读取输入的流时,我们应该使用 while 循环,并且只有在客户端发送终止请求时退出. 我们将在下一节中改进服务器代码。
客户端
客户端必须知道运行服务器的计算机的主机名或IP以及服务器正在监听的端口号。
客户端向服务器发出连接请求. 由于服务器与客户端在同一台电脑上, 所以服务器IP就是本地IP(127.0.0.1).
Socket clientSocket = new Socket("127.0.0.1", 6666);
在连接过程中, 客户端还需要向服务器标识(identify)自己. 这个过程我们不需要手动做.
上面的构造方法只在服务器接受连接时才实例一个新socket,连接失败,会抛出连接拒接异常(connection refused exception). 新socket创建成功时, 我们可以获得与服务器通讯的输入和输出流.
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
客户端的输入流连接到服务器的输出流,同理, 服务器的输入流连接到客户端的输出流.
持续通讯
我们当前的服务器在连接到客服端前, 一直处于阻塞状态. 连接客户端后, 服务器又再次阻塞来监听来自客户端的消息,在单个消息之后,它关闭连接,不再接受下一个客户端的通讯, 因为我们没有处理连续性。但想象一下我们想实现一个聊天服务器,肯定需要在服务器和客户端之间连续来回通信。
我们创建一个while循环来持续观察服务器的输入流以获取传入的消息。
让我们创建一个名为EchoServer.java的新服务器,其唯一目的是回应来自客户端的任何消息:
public class EchoServer {
public void start(int port) {
serverSocket = new ServerSocket(port);
clientSocket = serverSocket.accept();
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
if (".".equals(inputLine)) {
out.println("good bye");
break;
}
out.println(inputLine);
}
}
请注意,我们添加了一个终止条件,当我们收到一个句点字符(.
)时,while循环退出。
我们使用另一个端口 4444 来与上面的GreetServer
做区分.
下一个测试方法, 在EchoServer
没有关闭连接时发送多个请求. 这些请求全都是同一个客户端发的, 而处理不同客户端又是另一个情况, 我们以后会看到.
@Before
public void setup() {
client = new EchoClient();
client.startConnection("127.0.0.1", 4444);
}
我们将同样创建一个tearDown
方法来释放我们所有的资源,这是我们使用网络资源的最佳实践案例:
@After
public void tearDown() {
client.stopConnection();
}
然后让我们发送些请求测试我们的EchoServer
:
@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
String resp1 = client.sendMessage("hello");
String resp2 = client.sendMessage("world");
String resp3 = client.sendMessage("!");
String resp4 = client.sendMessage(".");
assertEquals("hello", resp1);
assertEquals("world", resp2);
assertEquals("!", resp3);
assertEquals("good bye", resp4);
}
多客户端的服务器
我们将在一个新的线程里, 为服务器和客户端创建一个新的socket, 服务器同时连接多少个客户端, 就创建多少个线程.
主线程将用 while
循环来监听是否有新的连接.
我们创建一个 EchoMultiServer.java
, 还有一个EchoClientHandler
内部类去管理每一个客户端的连接:
public class EchoMultiServer {
private ServerSocket serverSocket;
public void start(int port) {
serverSocket = new ServerSocket(port);
while (true)
new EchoClientHandler(serverSocket.accept()).start();
}
public void stop() {
serverSocket.close();
}
private static class EchoClientHandler extends Thread {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClientHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
if (".".equals(inputLine)) {
out.println("bye");
break;
}
out.println(inputLine);
}
in.close();
out.close();
clientSocket.close();
}
}
注意我们让accept
方法在while
循环内, 任何时候这个while
循环都在执行, 它一直在等待新的客户端连接, 然后为客户端创建处理线程EchoClientHandler
.
线程内执行类似于 EchoServer
里处理单个客户端的任务.
然我们的服务器监听 5555 端口. 并建立新的单元测试:
@Test
public void givenClient1_whenServerResponds_thenCorrect() {
EchoClient client1 = new EchoClient();
client1.startConnection("127.0.0.1", 5555);
String msg1 = client1.sendMessage("hello");
String msg2 = client1.sendMessage("world");
String terminate = client1.sendMessage(".");
assertEquals(msg1, "hello");
assertEquals(msg2, "world");
assertEquals(terminate, "bye");
}
@Test
public void givenClient2_whenServerResponds_thenCorrect() {
EchoClient client2 = new EchoClient();
client2.startConnection("127.0.0.1", 5555);
String msg1 = client2.sendMessage("hello");
String msg2 = client2.sendMessage("world");
String terminate = client2.sendMessage(".");
assertEquals(msg1, "hello");
assertEquals(msg2, "world");
assertEquals(terminate, "bye");
}
我们可以像上面一样用多个测试用例来创建多个客户端来测试服务器.
结论
在本教程中,我们重点介绍了基于TCP / IP的套接字编程,并用Java编写了一个简单的Client / Server应用程序。