指网络上的主机, 通过不同的进程, 以编程的方式实现 网络通信 (或称为网络数据传输) (同一台主机的不同进程间, 基于网路的通信也可以称为网络编程)
网络编程中的概念
服务端: 网络通信中, 提供服务的一方 (进程)
客户端: 网络通信中, 获取服务的一方 (进程)
Socket 套接字, 是由系统提供的, 用于网络通信的技术, 是基于 TCP/IP 协议的, 网络通信的基本操作单元. 基于 Socket 套接字的网络程序开发就是网络编程
Socket 套接字根据 针对的传输层协议 可以分为三类:
TCP (Transmission Control Protocol) 传输控制协议 (传输层协议), 特点:
UDP (User Datagram Protocol) 用户数据报协议 (传输层协议), 特点:
短连接: 每次接收到数据并返回响应后, 都会关闭连接. (短连接只能一次收发数据)
长连接: 不关闭连接, 一直保持连接状态, 双方不停的收发数据. (长连接可以多次收发数据)
长短连接拥有不同的特点 :
长连接有两种实现方式, 基于 BIO 的长连接和基于 NIO 的长连接
对于 发送数据 时的 数据包装动作 来说
对于 接收数据 时的 数解析动作 来说
一般根据字段的特点进行设计
除此之外, 协议中还会包含: 状态码, 请求类型 等等内容
ServerSocket.accept()
方法建立连接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.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
// 此处使用 CachedThreadPool, 使用 FixedThreadPool 不太合适 (线程数量不应该固定)
ExecutorService threadPool = Executors.newCachedThreadPool();
while(true) {
Socket clientSocket = serverSocket.accept();
threadPool.submit(() -> {
processConnection(clientSocket);
});
}
}
// 使用该方法来处理一个连接
// 这一个连接对应一个客户端 (一次连接可能会涉及到多次交互)
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 基于上述 socket 对象和客户端进行通讯
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 由于要处理多个请求和响应, 因此使用循环来进行
while(true) {
// 1. 读取请求
Scanner sc = new Scanner(inputStream);
if(!sc.hasNext()) {
// 没有下个数据, 说明读完了. (客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!\n",
clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
// 注意, 此处使用 next 是一直读取到 换行符/空格/其他空白符 结束, 但是最终结果不包含上述 空白符(因此后面要自己再补一个空白符).
String request = sc.next();
// 根据请求构造响应
String response = process(request);
// 3.返回响应结果.
outputStream.write((response+"\n").getBytes(StandardCharsets.UTF_8));
outputStream.flush();
// OutputString 没有 write(String) 这样的功能. 可以把 String 里的字节数组拿出来, 进行写入;
// 也可以使用 字符流 来转换
// PrintWriter printWriter = new PrintWriter(outputStream);
// 此处使用 println 来输出 response, 让结果中带有一个 \n 换行. 方便对端来接收解析.
// printWriter.println(response);
// flush 用来刷新缓冲区, 保证当前写入的数据, 一定会发送出去(println 输出后有可能会先放在缓冲区, 等缓冲区满会自动输出)
// 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 {
// finally 里的内容保证一定可以执行到
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String ip, int port) throws IOException {
// Socket 构造方法, 能够识别 点分十进制 格式的 ip 地址, 比 DatagramPacket 更方便.
// new 对象的同时, 就会进行 Tcp 连接操作.
socket = new Socket(ip, port);
}
public void start() {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true) {
// 1. 先从键盘上读取用户输入的内容
System.out.print(">");
String request = scanner.next();
if(request.equals("bye")) {
System.out.println("goodbye");
break;
}
// 2. 把读取的内容构造成请求, 发送给服务器.
outputStream.write((request+"\n").getBytes(StandardCharsets.UTF_8));
outputStream.flush();
// PrintWriter printWriter = new PrintWriter(outputStream);
// printWriter.println(request);
// // flush 保证数据不会停留在缓冲区
// printWriter.flush();
// 3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应内容显示到界面上
System.out.println(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();
}
}
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// UDP 版本的回显服务器
public class UdpEchoServer {
// 网络编程, 本质上是要操作网卡
// 但是网卡不方便直接操作. 在 操作系统内核 中, 使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡
// 因此进行网络通信, 势必要现有一个 socket 对象
private DatagramSocket socket = null;
// 对于服务器来说, 创建 socket 对象的同时,要让他绑定上一个具体的端口号
// 服务器一定要关联上一个具体的端口号!!!
// 服务器是网络传输中, 被动的一方. 如果是操作系统随机分配的端口, 此时客户端就不知道这个端口是啥了, 也就无法进行通信了
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 服务器不是只给一个客户端提供服务就完了. 需要服务很多客户端
while(true) {
// 只要有客户端过来, 就可以提供服务
// 1. 读取客户端发来的请求是啥
// receive 方法的参数是一个输出型参数, 需要先构造好一个空白的 DatagramPacket 对象. 交给 receive 来进行填充
DatagramPacket requestPacket = new DatagramPacket(
new byte[4096] ,0, 4096);
socket.receive(requestPacket);
// 此时这个 DatagramPacket 是一个特殊的对象, 但不方便直接进行处理. 可以把这里包含的数据拿出来, 构造成一个字符串, 以便处理
// requestPacket.getLength(): 是数据内容的长度 requestPacket.getData().length: 是构成 packet 的字节数组的容量
String request = new String(
requestPacket.getData(),
0, requestPacket.getLength()); //new String 后面的参数是要数据内容的长度
// 2. 根据请求计算响应, 由于此处是回显服务器, 相应和请求相同
String response = process(request);
// 3. 把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好.
// 此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造. //SocketAddress 里面有 address 和 port (IP和端口号)
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(), 0 ,
response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印本次请求相应的处理中间结果
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 {
// 端口号的指定, 1024 ~ 65535 均可
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
// UDP 版本的回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
// 存储的是服务器的 ip 和 端口号, 以便通讯使用
private String serverIp;
private int serverPort;
// 一次通信, 需要有两个 ip, 两个 端口
// 客户端的 ip : 127.0.0.1 已知
// 客户端的 port 是系统自动分配
// 服务器的 ip 和 端口, 需要手动告诉客户端, 才能顺利把消息发送给服务器
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner sc = new Scanner(System.in);
while(true) {
// 1. 从控制台读取要发送的数据
System.out.println(">");
String request = sc.next();
if(request.equals("bye")) {
System.out.println("goodbye!");
break;
}
// 2. 构造成 UDP 请求, 并发送
// 构造这个 Packet 的时候, 需要把 severIp 和 port 都传进来. 但是此处 IP 地址需要填写一个 32 位的整数形式
// 上述的 IP 地址是一个字符串. 需要使用 InetAddress.getByName 来进行一个转换.
DatagramPacket requestPacket = new DatagramPacket(
request.getBytes(), 0, request.getBytes().length,
InetAddress.getByName(this.serverIp), this.serverPort);
socket.send(requestPacket);
// 3. 读取服务器的 UDP 响应, 并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 0, 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0 , responsePacket.getLength());
// 4. 把解析好的结果显示出来
System.out.printf("[%s:%d] response: %s; request: %s;\n",
responsePacket.getAddress().toString(),
responsePacket.getPort(), response, request);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}