紧接着
Java Socket通信之UDP协议 再来!
一、 Java流套接字通信模型
1.TCP模型
TCP的整个通信流程如下如所示: 服务器的任务: ①创建ServerSocket对象并绑定一个端口,成为listenSocket(类似于大街拉拢客人的中介)。 ②listenSocket调用ServerSocket的accept()方法,把内核建立好的连接拿到代码中进行处理。如果接收到来自客户端的请求时,accept()会返回一个Socket实例,称为clientSocket(类似于中介把客人介绍给前台小姐姐,由她负责之后的业务流程,中介继续去拉拢客人),如果没有接收到请求,则一直处于阻塞等待状态。 ③使用clientSocket的getInputStream和OutputStream得到字节流对象,可进行字节的读取和写入。 ④当客户端断开连接后,服务器要及时关闭clientSocket,否则会出现文件资源泄露的情况。 客户端的任务: ①创建一个Socket对象,同时指定服务器的IP和Port (建立TCP连接三次握手,由内核完成,用户感知不到)。 ②客户端通过Socket对象getInputStream和getOutputStream和服务器进行通信。 关于端口被占用问题: 通常情况下,两个进程无法绑定到同一个端口号!(除特殊情况:eg. Linux中 fork系统调用) 解决办法:如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B;如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。
2.TCP Socket常见API
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API; ServerSocket构造方法:
方法 | 说明 |
---|
ServerSocket(int port) | 创建一个服务端流套接字socket,并绑定到响应端口上 |
ServerSocket方法:
方法 | 说明 |
---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),如果有客户端连接,返回一个socket对象,如果没有,一直处于阻塞等待状态。(用户代码调用accept,才是真的把连接拿到用户代码中) | void close() | 关闭此套接字(否则随着连接的增多,socket文件会出现文件资源泄露的情况) |
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对方端信息,即用来与对方收发数据的。 注:Socket是服务器和客户端都需要使用;ServerSocket是只在服务器中使用的。 Socket 构造方法:
方法 | 说明 |
---|
Socket(String host, int post) | 创建一个客户端流套接字socket,并与对应IP和port的进程建立连接 |
Socket 方法:TCP Socket 是基于字节流的,进行具体读写时和文件类似。
方法 | 说明 |
---|
InetAddress getInetAddress() | 返回套接字所连接的地址 | InputStream getInputStream() | 返回此套接字的输入流 | OutputStream getOutputStream() | 返回此套接字的输出流 |
二、TCP流套接字编程
1.回显服务器
服务器代码:
package TCPEcho;
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;
public class TcpEchoServer {
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
while (true) {
Socket clientSocket = listenSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
String log = String.format("[%s:%d]客户端已上线!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
while(true) {
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
log = String.format("[%s:%d]客户端已下线!",
clientSocket.getInetAddress().toString(), clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
log = String.format("[%s:%d],request: %s, response: %s",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
System.out.println(log);
}
}catch(IOException e){
e.printStackTrace();
}finally{
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客户端代码:
package TCPEcho;
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 = null;
private String serverIP;
private int serverPort;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
this.serverIP = serverIP;
this.serverPort = serverPort;
this.socket = new Socket(serverIP,serverPort);
}
public void start() throws IOException {
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(System.in);
System.out.println("->");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("程序退出!");
break;
}
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
String log = String.format("request:%s, response:%s",request,response);
System.out.println(log);
}
}catch(IOException e){
e.printStackTrace();
}finally{
socket.close();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
tcpEchoClient.start();
}
}
2.多线程服务器
上面的例子,我们只能实现一个客户端一个服务器这样的通信方式,如果多个客户端共同发来请求,就要用到多线程;更进一步,线程的创建和销毁都需要消耗资源,为了避免频繁创建销毁线程,我们也可以使用线程池进行代码的改进。 服务器代码:
package TCPThreadEcho;
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;
public class TcpThreadEchoServer {
private ServerSocket listenSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = listenSocket.accept();
Thread t = new Thread() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
t.start();
}
}
private void processConnection(Socket clientSocket) throws IOException {
String log = String.format("[%s:%d]客户端已上线!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
log = String.format("[%s:%d]客户端已下线!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
log = String.format("[%s:%d],request:%s, response:%s",
clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
System.out.println(log);
}
}catch(IOException e){
e.printStackTrace();
}finally{
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
改进:
package TCPThreadPoolEcho;
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 TcpThreadPoolEchoServer {
private ServerSocket listenSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = listenSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
String log = String.format("[%s:%d]客户端已上线!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
log = String.format("[%s:%d]客户端已下线!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
log = String.format("[%s:%d],request:%s, response:%s",
clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
System.out.println(log);
}
}catch(IOException e){
e.printStackTrace();
}finally{
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer tcpThreadPoolEchoServer = new TcpThreadPoolEchoServer(9090);
tcpThreadPoolEchoServer.start();
}
}
客户端代码:
package TCPThreadEcho;
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 TcpThreadEchoClient {
public Socket socket = null;
public TcpThreadEchoClient(String serverIp, int serverPort) throws IOException {
this.socket = new Socket(serverIp, serverPort);
}
public void start(){
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while (true){
Scanner scanner = new Scanner(System.in);
String request = scanner.next();
if(request.equals("exit")){
System.out.println("程序退出");
break;
}
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
String log = String.format("[%s:%d],request:%s,response:%s",
socket.getInetAddress().toString(),socket.getPort(),request,response);
System.out.println(log);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpThreadEchoClient tcpThreadEchoClient = new TcpThreadEchoClient("127.0.0.1", 9090);
tcpThreadEchoClient.start();
}
}
针对线程特别多的情况如何处理?(高并发解决!) ①协程代替线程,完成并发 ②IO多路复用机制,完成并发 IO多路复用机制 ③使用分布式,提供更多的硬件资源 TCP协议的抓包工具:tcpdump(Linux)、wireShark跨平台、图形化工具。
三、TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接: 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
两者对比如下: ①建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。 ②主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。 ③两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。 基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
四、协议
1. 为什么需要协议?
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式: ①客户端发送请求和服务端解析请求要使用相同的数据格式。 ②服务端返回响应和客户端解析响应也要使用相同的数据格式。 ③请求格式和响应格式可以相同,也可以不同。 ④约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。 ⑤可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
2. 封装/分用 VS 序列化/反序列化
一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换格式,对发送数据时的数据包装动作来说: 如果是使用知名协议,这个动作也称为封装 如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。 接收端应用程序,接收数据时的数据转换格式,即对接收数据时的数据解析动作来说: 如果是使用知名协议,这个动作也称为分用 如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象。
3. 自定义协议
在数据传输的过程中,除了UDP和TCP协议外,程序还存在应用层自定义协议;对于协议来说,重点需要约定好如何解析,一般是根据字段的特点来设计协议: 对于定长的字段:可以基于长度约定,如int字段,约定好4个字节即可 对于不定长的字段: ①可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔,\n符号结束等 ②除了该字段“数据”本身,再加一个长度字段,用来标识该“数据”长度;即总共使用两个字段: “数据”字段本身,不定长,需要通过“长度”字段来解析; “长度”字段,标识该“数据”的长度,即用于辅助解析“数据”字段。
|