网络编程之Socket & ServerSocket
Java Socket 编程
Overview
socket编程旨在通过网络让运行再不同的电脑上的程序能相互通讯。
下面将介绍据于TPC/IP**的教程,使用Java编写客户端/服务器(C/S)应用程序。
学这个教程前, 你应该已经知道:
- 基本的计算机网络
- 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应用程序。
目录
网络编程之Socket & ServerSocket
Socket:网络套接字,网络插座,建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;socket用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。
1、客户端Socket类
此类实现客户端套接字
构造方法
构造方法 | 作用 |
---|---|
Socket(String host, int port) | 创建一个套接字,并将其连接到指定的主机的指定端口上 |
参数
String host:服务器主机的名称、服务器的IP地址
int port:服务器的端口号
成员方法
成员方法 | 作用 |
---|---|
OutputStream getOutputStream() | 返回此套接字的字节输出流 |
InputStream getInputStream() | 返回此套接字的字节输入流 |
void close() | 关闭此套接字 |
实现步骤
- 创建一个客户端对象Socket,构造方法中绑定服务器的IP地址,端口号
- 使用Socket对象中的方法getOutputStream获取网络字节输出流OutputStream对象
- 使用网络字节输出流OutputStream对象的方法write,给服务器发送信息
- 使用Socket对象的方法getInputStream获取网络字节输入流InputStream对象
- 使用网络字节输入流InputStream的方法read读取服务器返回的信息
- 调用close方法关闭套接字,释放资源
注意:
- 当客户端与服务器交互时,必须使用Socket中提供的网络流,不能使用自己创建的流对象
- 当创建客户端Socket对象时,就会去请求服务器,和服务器进行三次握手建立TCP连接。如果此时服务器没有启动,就会抛出异常,否则就可以进行交互了。
TCP的客户端代码:
package cn.zhuobo.day15.aboutSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) throws IOException {
Socket so = new Socket("127.0.0.1", 8868);
OutputStream outputStream = so.getOutputStream();
outputStream.write("你是他妈的服务器吗".getBytes());
InputStream inputStream = so.getInputStream();
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
System.out.println(new String(bytes, 0, len));
so.close();
}
}
2、服务器套接字ServerSocket
此类实现服务端套接字
构造方法:
构造方法 | 作用 |
---|---|
ServerSocket(int port) | 创建一个服务器套接字,并绑定到特定的端口号上 |
成员方法:
成员方法 | 作用 |
---|---|
Socket accept() | 监听并接收此套接字的连接,用这个方法获取请求连接的客户端Socket对象 |
void close() | 关闭此套接字 |
服务器ServerSocket的实现过程
- 创建服务器ServerSocket对象,并指定需要的端口号
- 使用ServerSocket对象的accept方法获取请求的客户端Socket对象
- 使用获取到的Socket对象的getInputStream方法,获取网络字节输入流InputStream对象
- 使用网络字节输入流InputStream对象的read方法,读取客户端发送的数据
- 使用获取到的Socket对象的getOutputStream方法,获取网络字节输出流OutputStream对象
- 使用网络字节输出流OutputStream队形的write方法,给客户端写回数据
- 释放资源,Socket对象和ServerSocket对象依次调用close方法
服务器ServerSocket代码:
package cn.zhuobo.day15.aboutSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServerSocket {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8868);
Socket so = server.accept();
InputStream inputStream = so.getInputStream();
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
System.out.println(new String(bytes, 0, len));
OutputStream outputStream = so.getOutputStream();
outputStream.write("我是他么的服务器,你是他么的客户端吗".getBytes());
so.close();
server.close();
}
}
这样就客户端和服务器就实现了通信
3、文件上传案例
文件的上传其实就是本地硬盘的文件复制到服务器的硬盘
注意
客户端、服务器和本地进行文件读写,使用的是自己创建道德字节流对象(本地流)
客户端和服务器之间的文件读写,使用的是Socket提供的字节流对象(网络流)
Client代码
package cn.zhuobo.day15.fileUploadPractise;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("/home/zhuobo/Desktop/dir/1.png");
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream();
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
socket.shutdownOutput();
InputStream inputStream = socket.getInputStream();
while((len = inputStream.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
}
fis.close();
socket.close();
}
}
Server代码
package cn.zhuobo.day15.fileUploadPractise;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while(true) {
Socket socket = server.accept();
new Thread(new Runnable() {// 每次侦听到有客户端程序要上传文件,也就是accept方法后就开启一个新的线程,提高效率
@Override
public void run() {
try {// 因为run方法没有声明抛出异常,因此只能使用try catch处理异常
InputStream inputStream = socket.getInputStream();
// 如果不存在这个目录就创建
File file = new File("/home/zhuobo/Desktop/uploads");
if(!(file.exists())) {
file.mkdirs();
}
// 文件命名的规则
String filename = "/" + System.currentTimeMillis() + (new Random().nextInt(99999)) + ".png";
FileOutputStream fos = new FileOutputStream(file + filename);
byte[] bytes = new byte[1024];
int len = 0;
while((len = inputStream.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
socket.getOutputStream().write("文件上传成功!".getBytes());
fos.close();
socket.close();
}catch (IOException e) {
System.out.println(e);
}
}
}).start();
}
//server.close();
}
}