socket API
网络编程套接字是操作系统给应用程序提供的一组API(叫做socket API)
socket可以视为应用层和传输层之间的通信桥梁
socket API也有对应的两组
由于上一节课(网络编程1)我们看到TCP和UDP差别很大,因此API差别也很大
下面我们来讲一下UDP socket和TCP socket
先讲UDP socket
UDP socket中主要涉及两个类:
1.DatagramSocket
一个DatagramSocket对象就对应到操作系统中的一个socket文件
socket文件就对应着"网卡"这种硬件设备.从socket文件读数据,本质上就是读网卡,从socket文件写数据,本质上就是写网卡.socket就像一个遥控器,它可以操作网卡.
2.DatagramPacket
DatagramPacket代表了一个UDP数据报,他是使用UDP传输数据的基本单位,每次发送/接收数据都是在传输一个DatagramPacket对象
下面我们来写一下最简单的
UDP的客户端服务器程序---回显服务(请求内容是什么,得到的响应就是什么)
回显服务器:UdpEchoServer
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// 站在服务器的角度:
// 1. 源 IP: 服务器程序本机的 IP
// 2. 源端口: 服务器绑定的端口 (此处手动指定了 9090)
// 3. 目的 IP: 包含在收到的数据报中. (客户端的IP)
// 4. 目的端口: 包含在收到的数据报中. (客户端的端口)
// 5. 协议类型: UDP
public class UdpEchoServer {
// 进行网络编程, 第一步就需要先准备好 socket 实例~ 这是进行网络编程的大前提.
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 启动服务器.
public void start() throws IOException {
System.out.println("启动服务器!");
// UDP 不需要建立连接, 直接接收从客户端来的数据即可
while (true) {
// 1. 读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(requestPacket); // 为了接受数据, 需要先准备好一个空的 DatagramPacket 对象, 由 receive 来进行填充数据
// 把 DatagramPacket 解析成一个 String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
// 2. 根据请求计算响应(由于咱们这是一个回显服务, 2 省略)
String response = process(request);
// 3. 把响应写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n",
requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
// 由于是回显服务, 响应就和请求一样了.
// 实际上对于一个真实的服务器来说, 这个过程是最复杂的. 为了实现这个过程, 可能需要几万行, 几十万行代码....
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
下面是对代码的一些解释:
多个进程不能绑定同一个端口,但一个进程可以绑定多个端口---->我们就把端口想象成手机号,几个人不能共用一个手机号,但是一个人可以有多个手机号
如果一个程序需要使用网络通信,那么至少需要一个端口号(就像一个人要想,他至少需要一个手机号码)
回显客户端:UdpEchoClient
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 站在客户端的角度:
// 源 IP: 本机 IP
// 源端口: 系统分配的端口
// 目的 IP: 服务器的 IP
// 目的端口: 服务器的端口
// 协议类型: UDP
public UdpEchoClient(String ip, int port) throws SocketException {
// 此处的 port 是服务器的端口.
// 客户端启动的时候, 不需要给 socket 指定端口. 客户端自己的端口是系统随机分配的~~
socket = new DatagramSocket();
serverIP = ip;
serverPort = port;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 先从控制台读取用户输入的字符串
System.out.print("-> ");
String request = scanner.next();
// 2. 把这个用户输入的内容, 构造成一个 UDP 请求, 并发送.
// 构造的请求里包含两部分信息:
// 1) 数据的内容. request 字符串
// 2) 数据要发给谁~ 服务器的 IP + 端口
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3. 从服务器读取响应数据, 并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
// 4. 把响应结果显示到控制台上.
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
// 由于服务器和客户端在同一个机器上, 使用的 IP 仍然是 127.0.0.1 . 如果是在不同的机器上, 当然就需要更改这里的 IP 了
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
下面是对代码的一些解释:
这里我们介绍一下--->五元组
写代码的时候就会涉及到一系列的IP和端口,五元组就是--->一次通信是由五个核心信息描述出来的---->源IP,源端口,目的IP,目的端口,协议类型
上面我们写的服务器和客户端的五元组如下图所示
注意,通常情况一个服务器可以给多个客户端提供服务
这里提一个小醒
--->当我们有了服务器和客户端,但我还想再启动一个客户端的时候,由于IDEA默认情况下,一个程序只能启动一个实例,因此IDEA会提示让我们把上一个客户端干掉才能启动另一个.
解决办法如下:
上面我们写的服务器和客户端就是最简单的程序,下面我们带上一点业务逻辑看一下
我们写一个翻译程序(英译汉),请求是一些简单的英文单词,响应就是英文单词对应的翻译
我们让客户端不变,把服务器代码进行调整(这里主要调整的是process方法)
翻译服务器:UdpDictServer
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 简单构造几个词
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fuck", "卧槽");
dict.put("pig", "小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "该词无法被翻译!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
好了,上面我们写了UDP的客户端服务器程序,下面我们来写一下TCP的客户端服务器程序
TCP API中也涉及到两个核心的类
回显服务器:TcpEchoServer
package network;
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 {
// listen => 英文原意 监听~~
// 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
// 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话~~
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来来处理请求和响应
// 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
String request = scanner.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 此处要记得来个关闭操作.
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端:TcpEchoClient
package network;
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 {
// 用普通的 socket 即可, 不用 ServerSocket 了
// 此处也不用手动给客户端指定端口号, 让系统自由分配.
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
// 其实这里是可以给的. 但是这里给了之后, 含义是不同的. ~~
// 这里传入的 ip 和 端口号 的含义表示的不是自己绑定, 而是表示和这个 ip 端口建立连接!!
// 调用这个构造方法, 就会和服务器建立连接 (打电话拨号了)
socket = new Socket(serverIP, serverPort);
}
public void start() {
System.out.println("和服务器连接成功!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream()) {
try (OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 要做的事情, 仍然是四个步骤
// 1. 从控制台读取字符串
System.out.print("-> ");
String request = scanner.next();
// 2. 根据读取的字符串, 构造请求, 把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据.
// 3. 从服务器读取响应, 并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把结果显示到控制台上.
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
虽然现在我们的TCP的客户端服务器程序已经跑起来了,但是有一个问题!--------->当前我们写的的TCP服务器程序只能处理一个客户端
那么为什么我们的TCP服务器程序现在只能处理一个客户端呢?
原因是:
像我们上面写的UDP程序一启动就可以读写了
但是,TCP程序启动,客户端需要先建立连接,服务器需要先接受连接(调用accept),然后才能读写
我们来看下我们当前写的TCP服务器的accept是怎么调用的
那么我们怎么解决这个问题呢?
要想解决这个问题,就得让processConnection的执行和前面的accept的执行互不干扰,不能让processConnection里面的循环导致accept无法及时调用----------因此解决办法是引入多线程 或者 引入线程池
现在你可能有一个问题,问什么我们写UDP版本的程序没用多线程就好着呢?
因为UDP不需要处理链接呀,我们上面有说UDP程序一启动就可以读写了 ,UDP只要一个循环,就可以处理所有客户端的请求
但是此处TCP既要处理连接,又要处理一个连接中的若干次请求,因此就需要两个循环
主线程循环调用accept,当有客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干请求提供服务(在新线程里通过while循环来处理请求),这时多个线程是并发执行的关系,就不会互相干扰
下面我们分别来看下引入多线程的TCP程序和引入线程池的TCP程序
引入多线程:--->回显服务器:TcpThreadEchoServer
改动的地方:
整体代码:
package network;
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 {
// 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
// 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话~~
Socket clientSocket = serverSocket.accept();
// [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来来处理请求和响应
// 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
String request = scanner.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 此处要记得来个关闭操作.
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
引入线程池:--->回显服务器:TcpThreadPoolEchoServer
改动的地方:
整体代码:
package network;
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 {
// 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
// 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话~~
Socket clientSocket = serverSocket.accept();
// [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
// 通过线程池来实现
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来来处理请求和响应
// 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
String request = scanner.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 此处要记得来个关闭操作.
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
server.start();
}
}