文章目录
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输:能够保证收到,收不到数据,会有相应的回应,一定是按照顺序接收的,数据可以分批次接收
- 面向字节流
面向字节流需要注意的问题:
为了区分字节流中不同的请求,可以
1)规定请求是固定长度
2)规定先发送请求的长度再发送请求
3)使用特殊符号分割请求,如\r\n
4)一个连接中,只发送一个请求(短连接)
1 TCP socket API
ServerSocket类
ServerSocket(int port)
创建绑定到指定端口的服务器套接字
ServerSocket(int port, int backlog)
创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。
Socket accept()
侦听要连接到此套接字的连接并接收
bind(SocketAddress endpoint)
将ServerSocket绑定到特定地址(IP地址和端口号)
InetAddress getInetAddress()
返回此服务器套接字的本地地址
void close()
关闭此套接字
int getLocalPort()
返回此套接字正在侦听的端口号
bind(): 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;如果地址为null ,则系统将接收临时端口和有效的本地地址来绑定套接字。
accept(): 三次握手完成后, 服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;Socket 是一个返回值,代表网络的套接字
Socket类
Socket(InetAddress address, int port)
创建流套接字并将其连接到指定IP地址的指定端口号
Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号
void bind(SocketAddress bindpoint)
将套接字绑定到本地地址
void connect(SocketAddress endpoint)
将此套接字连接到服务器
InetAddress getInetAddress()
返回套接字所连接的地址
InputStream getInputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
2 短连接:
1)服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
// 有连接的
public class Server {
public static void main(String[] args) throws IOException {
// 1. 创建 TCP 的 Server Socket
// 1. 创建(create) socket
// 2. 绑定(bind) socket <-> 本地 ip + 本地 port
// 3. 监听(listen) socket 等待客户端过来连接
try (ServerSocket serverSocket = new ServerSocket(9527)) {
while (true) {
// 不断的接待客户
// 和 UDP 不同,不是以请求为单位接待,而是以连接(Connection) (client <-> server)
try (Socket connectionSocket = serverSocket.accept()) {
// TCP 是面向字节流的
InputStream is = connectionSocket.getInputStream();
// OutputStream os = connectionSocket.getOutputStream();
// IO 流中的字节流
// is 可以读取 client 发送过来的数据
// os 可以把写给 client 的数据发送过去
// 把字节流封装成字符流
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
// OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
// 封装成 Scanner 和 PrintWriter 方便读写
//Scanner isScanner = new Scanner(isReader);
//PrintWriter osPrintWriter = new PrintWriter(osWriter);
// 一个连接中只会发送一个请求
char[] receiveBuffer = new char[8192]; // 假设请求肯定不会这么长
int len = isReader.read(receiveBuffer);
System.out.println(isReader.read()); // 期待读到 -1,期待读到 EOS,即请求终止
String request = new String(receiveBuffer, 0, len);
System.out.println("%" + request + "%");
}
}
}
}
}
注意:
ServerSocket serverSocket = new ServerSocket(PORT)
没有写ip,就是默认绑定本地所有ip地址。
2) 客户端
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
// 1. 创建 socket
// 2. 绑定本地 ip + 本地 port(OS 分配)
// 3. 连接远端 ip + 远端 port(127.0.0.1:9527)
// 面向连接
try (Socket socket = new Socket("127.0.0.1", 9527)) {
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
printWriter.print("|发送一个请求|");
printWriter.flush();
}
}
}
当服务端读到-1是即认为一次请求终止。
System.out.println(isReader.read());
// 期待读到 -1,期待读到 EOS,即请求终止
3 长连接(一个连接中可以发送多个请求,以\r\n来分割请求)
1)服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
// 一个连接中有多个请求
// 长连接 + 根据 \r\n 来分割请求和响应
public class Server {
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(8192)) {
while (true) {
try (Socket socket = serverSocket.accept()) { // 等待有客户端连接上来
// 那个客户端先连接上来,就处理哪个客户端的事情
// 服务器在专心处理 客户端1 的事情
System.out.println("======================================");
System.out.println("有一个客户端连接上来了");
InputStream is = socket.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
Scanner scanner = new Scanner(isReader);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
// 一个连接可以发送多个请求
// 服务器在这个循环中专心处理客户端1的事情
// 不知道客户端2已经连接上来了
// 直到客户端1关闭连接了(hasNextLine() 返回 false)
// 服务器才能重新处理下一个客户端连接
while (scanner.hasNextLine()) {
String request = scanner.nextLine(); // 客户端1 没有发送数据,这个方法就不会返回
System.out.println("++++++++++++++++++");
System.out.println(request);
// 处理请求 -> 响应
String response = request;
// 发送响应,也需要带着 \r\n
printWriter.println(response);
printWriter.flush(); // 千万不要忘掉
}
System.out.println("======================================");
}
}
}
}
}
2)客户端
import java.io.*;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Scanner stdinScanner = new Scanner(System.in);
try (Socket socket = new Socket("127.0.0.1", 8192)) {
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
InputStream is = socket.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
Scanner scanner = new Scanner(isReader);
//List<String> requestList = Arrays.asList("今天吃了么", "今天好冷啊", "今天好热啊", "回家吃去吧");
//for (String request : requestList) {
System.out.print("请输入请求> ");
while (stdinScanner.hasNextLine()) {
String request = stdinScanner.nextLine();
printWriter.println(request);
System.out.println("客户端请求 ->: " + request);
printWriter.flush();
String response = scanner.nextLine();
System.out.println("<- 服务器应答: " + response);
System.out.print("请输入请求> ");
}
}
}
}
注意:
while (scanner.hasNextLine())
相当于用\r\n来分割请求,达到在一次连接中处理多次请求的目的- 接收数据时(收到请求或响应),将输入流引到内存中,为了方便处理数据,一般都将输入流封装到Scanner中:
InputStream is = socket.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
Scanner scanner = new Scanner(isReader);
- 发送数据时(发送请求或响应),将输出流从内存中发送出去,为了方便处理数据,一般都将输出流封装到PrintWriter中。
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
- 在进行写操作时,一定不能忘了flush刷新操作
printWriter.flush();
。
4 多线程(实现同时为多个客户端服务):
1)服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
// 一个连接中有多个请求
// 长连接 + 根据 \r\n 来分割请求和响应
public class Server {
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(8192)) {
while (true) {
Socket socket = serverSocket.accept(); // 等待有客户端连接上来 - 主线程处理
// 下面的工作,交给另一个线程处理
ServerWaiter waiter = new ServerWaiter(socket);
waiter.start();
}
}
}
}
2)服务端线程任务处理
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ServerWaiter extends Thread {
private final Socket socket;
ServerWaiter(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
System.out.println("有一个客户端连接上来了");
InputStream is = socket.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
Scanner scanner = new Scanner(isReader);
// 没有 scanner,需要手动找 \r\n,麻烦的很
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
while (scanner.hasNextLine()) {
String request = scanner.nextLine(); // 客户端1 没有发送数据,这个方法就不会返回
System.out.println("++++++++++++++++++");
System.out.println(request);
// 处理请求 -> 响应
String response = request;
// 发送响应,也需要带着 \r\n
printWriter.println(response);
printWriter.flush(); // 千万不要忘掉
System.out.println("++++++++++++++++++");
}
System.out.println("======================================");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3)客户端
import java.io.*;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Scanner stdinScanner = new Scanner(System.in);
try (Socket socket = new Socket("127.0.0.1", 8192)) {
OutputStream os = socket.getOutputStream();
OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
PrintWriter printWriter = new PrintWriter(osWriter);
InputStream is = socket.getInputStream();
InputStreamReader isReader = new InputStreamReader(is, "UTF-8");
Scanner scanner = new Scanner(isReader);
System.out.print("请输入请求> ");
while (stdinScanner.hasNextLine()) {
String request = stdinScanner.nextLine();
printWriter.println(request);
System.out.println("客户端请求 ->: " + request);
printWriter.flush();
String response = scanner.nextLine();
System.out.println("<- 服务器应答: " + response);
System.out.print("请输入请求> ");
}
}
}
}
客户端的连接交给主线程,请求处理交给子线程来做
Socket socket = serverSocket.accept();
// 等待有客户端连接上来 - 主线程处理
// 下面的工作,交给另一个线程处理
ServerWaiter waiter = new ServerWaiter(socket);
waiter.start();
5 线程池处理多线程
1)服务端(采用线程池)
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
public static void main(String[] args) throws IOException {
// 因为线程只有 10 个
// 同时能处理的连接,也就只有 10 个
ExecutorService threadPool = Executors.newFixedThreadPool(10);
try (ServerSocket serverSocket = new ServerSocket(9527)) {
while (true) {
Socket socket = serverSocket.accept();
// socket 代表就是一个建立好的连接
threadPool.execute(new Worker(socket));
}
}
}
}
2)服务端线程任务
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Worker extends Thread {
private final Socket socket;
Worker(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(new InputStreamReader(is, "UTF-8"));
OutputStream os = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
// Client 主动关闭连接后,不再循环
while (scanner.hasNextLine()) {
String request = scanner.nextLine();
// 处理请求
String response = business(request);
// 发送响应
writer.println(response);
// 记得 flush
writer.flush();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String business(String request) {
return request;
}
}
3)客户端
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
try (Socket socket = new Socket("127.0.0.1", 9527)) {
// 建立连接,剩下的循环,全部在一个连接中完成
Scanner netScanner = new Scanner(new InputStreamReader(socket.getInputStream(), "UTF-8"));
PrintWriter netWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
while (true) {
String request = scanner.nextLine();
netWriter.println(request);
netWriter.flush();
String response = netScanner.nextLine();
System.out.println(response);
}
}
}
}
每开启一个客户端,服务端就会创建一个线程来执行任务,开启的线程执行完一次任务后会继续等待下一次任务的到来,当创建的线程数达到规定的正式线程数10时,就不会再创建了,多余的任务会移到堵塞队列中等待线程来执行。
线程池优点:
- 利用线程池管理并复用线程、控制最大并发数。 不需要每次都创建线程,销毁线程
- 实现任务线程队列缓存策略和拒绝机制。
- 实现某些与时间相关的功能,如定时执行、周期执行等。
- 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免个服务线程互相影响。
线程池缺点:
- 前期需要创建多个线程示例对象。
- 如果客户端连接少,会造成线程资源浪费