【Java】面向TCP接口的网络编程
一.基本通信模型
- TCP传输有连接;
- TCP传输面向字节流(Stream),无需构造数据报(Datagram)。
二. API
ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket
构造⽅法:
⽅法签名 | ⽅法说明 |
---|---|
ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ |
ServerSocket
⽅法:
⽅法签名 | ⽅法说明 |
---|---|
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客⼾端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
此处一定要注意的是:返回的服务端Socket对象并不是客户端Socket对象本体,而是相当于接听电话的关系,客户端拨通后,服务器连接。
Socket
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,即⽤来与对⽅收发数据的。
Socket
构造⽅法:
⽅法签名 | ⽅法说明 |
---|---|
Socket(String host, int port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接 |
Socket
⽅法:
⽅法签名 | ⽅法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输⼊流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
void close() | 关闭此套接字 |
三. 回显服务器/客户端示例
客户端
在编写客户端程序时,首先要考虑清楚通信模型,即如何通过字节流来进行通信,这里就要使用Socket
类中的getInputStream()
和getOutputStream()
方法来进行构建,同时还需要从终端读取请求,那还需要构建一个从终端读取请求的功能。
那么所搭建的基本框架如下:
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scannerConsole=new Scanner(System.in); //从终端读取请求
Scanner scannerNetwork=new Scanner(inputStream);//从输入流读取服务器返回的响应
PrintWriter writer=new PrintWriter(outputStream);//通过输出流向服务器发送请求
while(true){
//1.从终端读取请求
//2.把请求发送给服务器
//3.从服务器读取响应
//4.打印响应
}
}catch (IOException e){
throw new RuntimeException(e);
}
搭建好框架后,与UDP通信类似,这里客户端要做的也是四步:从终端读取请求、发送给服务器、读取服务器的响应、打印响应。
- 从终端读取请求
System.out.print(">");
if(!scannerConsole.hasNext()){
break;
}
String request=scannerConsole.next();
- 把请求发送给服务器
writer.println(request);
对于面向字节流的传输,只需将请求写入输出流即可进行传输。但是需要注意的是,服务器端接收时是通过服务器端的输入流接收,即通过Scanner next()
方法接收,而next()
方法读到空白符(空格,tab,回车…)才会截止,而我们做的 请求之中本身并没有空白符,因此此处通过println
来给请求手动加一个\n
结尾。
- 从服务器读取响应
String response=scannerNetwork.next();
同理,此处需要服务器返回的响应也要以空白符结尾。
- 打印响应
System.out.println(response);
服务器
在编写服务器端时,要考虑到TCP传输有连接、面向字节流的特点,因此设计的框架为:
public void start() throws IOException {
System.out.println("start!");
while(true){
//通过accept来“接听电话”,然后进行通信
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
//processConnection方法负责建立连接后的事务
public void processConnection(Socket clientSocket){
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
while(true){
//1.读取请求
//2.处理请求
//3.返回响应
//打印日志
}
}
}
- 读取请求
Scanner sc=new Scanner(inputStream);
if(!sc.hasNext()){
System.out.printf("客户端[%s:%d]下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
String request=sc.next();
- 处理请求
String response=process(request);
//编写process方法来处理请求
//对于回显服务器,只是为了观察通信过程,此处只返回请求作为响应
public String process(String request){
return request;
}
- 返回响应
//outputStream.write(response.getBytes(),0,response.getBytes().length);
PrintWriter writer=new PrintWriter(outputStream);
writer.println(response);
这里有两种写法,可以直接向构建的输出流中写入响应,也可以通过printWriter
来实现,这里之所以采用第二种,是为了给响应后加空白符。
- 打印日志
System.out.printf("[%s:%d]req:%s, resp:%s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
三个基本问题
理论上代码写到这里也就完成了,但实际还存在三个问题:
PrintWriter
内置缓冲区问题
IO操作都是比较低效的操作,此处PrintWriter
的内置缓冲区就是为了让这种低效操作尽可能少,简单理解就是需要填满这个缓冲区才会进行发送(合并多次IO操作为一次,提高效率)。
但正因如此,我们每次发送的数据量比较少,无法填满这个缓冲区,那么数据就会停留在这个缓冲区里,没有被真正发送出去。
解决方案:
writer.flush();
在每次客户端发送请求,服务器返回响应后,手动冲刷缓冲区,使数据得以发送。
- 服务器无法连接多个客户端
此处运行客户端1能够正常通信,而运行客户端2却无法返回响应。对于一个服务器来说,如果只能连接一个客户端,显然是不合理的。之所以造成这个问题,是因为服务器在接收到客户端1的请求后,即进入processConnection
的内层循环,就无法建立新的连接。
解决方案:
引入多线程/线程池:
此处展示线程池的使用:
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
//通过accept来“接听电话”,然后进行通信
Socket clientSocket=serverSocket.accept();
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
即通过主线程来接收客户端的请求,然后分配线程进行通信。
线程池相对于多线程呢,可以减少频繁创建/销毁线程的开销。
引入线程池后,问题可以很好的解决。
clientSocket
的关闭问题
在服务器连接多个客户端后,就会引入多个Socket对象,而如果不手动关闭这些Socket对象,堆积的Socket会越来越多,如果不释放,就很有可能把文件描述符表占满。
解决方案:
每次连接断开(processConnection 结束)后,关闭其对应的clientSocket:
finally {
try{
clientSocket.close();
}catch (IOException e){
throw new RuntimeException(e);
}
}
客户端完整代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
socket=new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动!");
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scannerConsole=new Scanner(System.in);
Scanner scannerNetwork=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while(true){
//1.从控制台读取请求
System.out.print(">");
if(!scannerConsole.hasNext()){
break;
}
String request=scannerConsole.next();
//2.把请求发送给服务器
writer.println(request);
writer.flush();
//3.从服务器读取响应
String response=scannerNetwork.next();
//4.打印响应
System.out.println(response);
}
}catch (IOException e){
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient("127.0.0.1",10100);
client.start();
}
}
服务器完整代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("start!");
//线程池处理
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
//通过accept来“接听电话”,然后进行通信
Socket clientSocket=serverSocket.accept();
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
public void processConnection(Socket clientSocket){
System.out.printf("客户端[%s:%d]上线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
while(true){
Scanner sc=new Scanner(inputStream);
if(!sc.hasNext()){
System.out.printf("客户端[%s:%d]下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
//1.读取请求,next读到空白符截止
String request=sc.next();
//2.处理请求
String response=process(request);
//3.返回响应(为了在response后加空白符)
//outputStream.write(response.getBytes(),0,response.getBytes().length);
PrintWriter writer=new PrintWriter(outputStream);
writer.println(response);
writer.flush();
//打印日志
System.out.printf("[%s:%d]req:%s, resp:%s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
}catch (IOException e){
throw new RuntimeException();
}finally {
try{
clientSocket.close();
}catch (IOException e){
throw new RuntimeException(e);
}
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(10100);
server.start();
}
}