TCP编程
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
- 对服务器端来说,它的Socket是指定的IP地址和指定的端口号;
- 对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。
服务器端(new ServerSocket)
要使用Socket编程,我们首先要编写服务器端程序。Java标准库提供了ServerSocket
来实现对指定IP和指定端口的监听。ServerSocket
的典型实现代码如下:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666); // 监听指定端口
System.out.println("server is running...");
for (;;) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}
class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}
private void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
writer.flush();
for (;;) {
String s = reader.readLine();
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
writer.flush();
}
}
}
服务器端通过代码:
ServerSocket ss = new ServerSocket(6666);
在指定端口6666
监听。
如果ServerSocket
监听成功,我们就使用一个无限循环来处理客户端的连接:
for (;;) {
Socket sock = ss.accept();
Thread t = new Handler(sock);
t.start();
}
注意到代码ss.accept()
表示每当有新的客户端连接进来后,就返回一个Socket
实例,这个Socket
实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket
创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。
我们在多线程编程的章节中介绍过线程池,这里也完全可以利用线程池来处理客户端连接,能大大提高运行效率。
如果没有客户端连接进来,accept()
方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket
会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()
就可以获取新的连接。
客户端(new Socket)
相比服务器端,客户端程序就要简单很多。一个典型的客户端程序如下:
public class Client {
public static void main(String[] args) throws IOException {
Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口
try (InputStream input = sock.getInputStream()) {
try (OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
}
sock.close();
System.out.println("disconnected.");
}
private static void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (;;) {
System.out.print(">>> "); // 打印提示
String s = scanner.nextLine(); // 读取一行输入
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if (resp.equals("bye")) {
break;
}
}
}
}
客户端程序通过:
Socket sock = new Socket("localhost", 6666);
连接到服务器端,注意上述代码的服务器地址是"localhost"
,表示本机地址,端口号是6666
。如果连接成功,将返回一个Socket
实例,用于后续通信。
Socket流
当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket
实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream
和OutputStream
来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:但要记得调用对象的 flush 方法
// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
最后我们重点来看看,为什么写入网络数据时,要调用flush()
方法。
如果不调用flush()
,我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()
强制把缓冲区数据发送出去。