目录
[一. 什么是socket套接字](#一. 什么是socket套接字)
[二. socket套接字](#二. socket套接字)
[2.1 socket套接字根据传输层协议分类](#2.1 socket套接字根据传输层协议分类)
[2.2 TCP流套接字 UDP数据报套接字主要特点](#2.2 TCP流套接字 UDP数据报套接字主要特点)
[三. UDP数据报套接字编程](#三. UDP数据报套接字编程)
[3.1 DatagramSocket 是UDP socket, 用于发送和接受数据报](#3.1 DatagramSocket 是UDP socket, 用于发送和接受数据报)
[3.2 DatagramPacket 是UDP socket 发送和接收的数据报](#3.2 DatagramPacket 是UDP socket 发送和接收的数据报)
[3.3 练习案例](#3.3 练习案例)
[四. TCP字节流套接字编程](#四. TCP字节流套接字编程)
[4.1 ServerSocket](#4.1 ServerSocket)
[4.2 Socket](#4.2 Socket)
[4.3 练习案例](#4.3 练习案例)
一. 什么是socket套接字
socket套接字是由系统提供用于网络通信的技术, 是基于TCP/IP协议的网络通信的基本操作单元.
基于Socket套接字的网络程序开发就是网络编程.
简单来说, socket就是网络编程套接字, 用来对网卡进行操作.
读网卡就是从网上上收数据, 写网卡就是让网卡发数据.
二. socket套接字
2.1 socket套接字根据传输层协议分类
socket套接字根据传输层协议分类:
字节流套接字 使用传输层TCP协议 Transmission Control Protocol(传输控制协议)
数据报套接字 使用传输层UDP协议 User Datagram Protocol(⽤⼾数据报协议)
2.2 TCP流套接字 UDP数据报套接字主要特点
TCP流套接字: 有连接, 可靠传输, 面向字节流, 全双工
UDP数据报套接字: 无连接, 不可靠传输, 面向数据报, 全双工
- 有连接 vs 无连接
TCP, 保存通信双方信息 (例如: A与B通信, A与B建立连接)
UDP, 不保存通信双方信息, 但是可以自己实现代码来保存双端信息.
- 可靠传输 vs 不可靠传输
TCP, 尽可能提高传输成功的概率, 如果出现丢包, 可以感知到.
UDP, 只要把数据发送了, 就不管了.
- 面向字节流 vs 面向数据报
TCP, 以字节为单位, 读写数据.
UDP, 以数据报为单位, 读写数据.
- 全双工 vs 半双工
一个通信链路中, 支持双向通信(能读也能写) => TCP/UDP
~, 支持单向通信(要么读, 要么写)
三. UDP数据报套接字编程
3.1 DatagramSocket 是UDP socket, 用于发送和接受数据报
构造方法
成员方法
3.2 DatagramPacket 是UDP socket 发送和接收的数据报
构造方法
成员方法
3.3 练习案例
回显服务器
java
package UDPNet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UDPEchoServer {
DatagramSocket socket = null;
public UDPEchoServer(int serverPort) throws IOException {
socket = new DatagramSocket(serverPort); // 指定服务器端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
executorService.submit(() -> {
while (true) { // 客户端发送多次请求
// 1. 接收请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket); // 输出型参数
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 计算响应
String response = process(request);
// 3. 返回响应
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 打印日志
System.out.printf("[%s:%d] req:%s res:%s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response);
}
});
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException{
UDPEchoServer udpEchoServer = new UDPEchoServer(3306);
udpEchoServer.start();
}
}
回显客户端
java
package UDPNet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UDPEchoClient {
DatagramSocket socket = null;
String serverIP;
int serverPort;
UDPEchoClient(String serverIP, int serverPort) throws IOException {
this.serverIP = serverIP;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 接收用户输入
String request = scanner.nextLine();
// 2. 将请求发送到客户端
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3. 接收服务器响应并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println("服务器的响应为: " + response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 3306);
udpEchoClient.start();
}
}
四. TCP字节流套接字编程
4.1 ServerSocket
用于创建TCP服务端socket.
构造方法
成员方法
4.2 Socket
用于创建TCP客户端socket, 或者是服务端与客户端建立连接(accept)后返回的服务端socket.
构造方法
成员方法
4.3 练习案例
回显服务器
java
package TCPNet;
import java.io.*;
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 {
// ServerSocket 服务器使用
// socket 服务器和客户端都使用
private ServerSocket serverSocket = new ServerSocket();
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port); // 给服务器分配端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executors = Executors.newCachedThreadPool();
while (true) { // 服务器可能与多个客户端建立连接
Socket socket = serverSocket.accept(); // 服务器与客户端建立连接, 返回的是服务端socket
// socket.getInetAddress 返回的是与服务器连接的客户端的IP地址.
// socket.getPort 返回的是与服务器建立连接的客户端的Port端口号
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
executors.submit(() -> {
try {
processConnection(socket); // 处理一次连接
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void processConnection(Socket socket) throws IOException {
try (socket; InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writerNet = new PrintWriter(outputStream);
while (true) { // 一次连接中可能会有多次请求
if (!scannerNet.hasNextLine()) { // 从网卡中读不到数据了
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
break;
}
// 1. 接收请求并解析
String request = scannerNet.nextLine();
// 2. 计算响应
String response = process(request);
// 3. 返回响应
writerNet.println(response); // 将response放在内存缓冲区中, 并没有让网卡发送数据到客户端
// 为什么要放在内存缓冲区中呢?
// 因为TCP是面向字节流的, 数据是零散的, 放在缓冲区中等数据达到一定数量再发送.
// 平衡了IO速度, 减少了发送次数, 提高了程序效率
writerNet.flush(); // 刷新缓冲区, 让网卡发送数据到客户端
// 打印日志
System.out.printf("[%s:%d] req:%s res:%s\n", socket.getInetAddress(), socket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 一个客户端断开连接, 关闭客户端
// 没有使用finally, 而是使用try with
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException{
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
回显客户端
java
package TCPNet;
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 TCPEchoClient {
Socket socket = null;
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
public void start() throws IOException{
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writerNet = new PrintWriter(outputStream);
while (true) { // 一次连接中多次请求
// 1. 用户输入请求
String request = scanner.nextLine();
// 2. 将请求发送到服务器
writerNet.println(request);
writerNet.flush();
// 3. 接收响应
String response = scannerNet.nextLine();
// 打印响应
System.out.println("服务器的响应为: " + response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException{
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
注意:
(1) 问题
如果只是单线程, 则服务器无法连接多个客户端, 这是因为服务端中的scannerNet.hasNextLine()阻塞, 导致不能进入外层循环, 服务器无法与其他客户端建立连接(accept).
(2) 解决
引入多线程和线程池, 把processConnection操作作为一个任务提交到线程池中. 就意味着一个客户端的连接相当于一个任务, 任务之间互不冲突.