
操作系统给应用程序(传输层给应用层)提供的API起了个名字就叫socket api socket本身含义是插槽,类似于电脑主板上的插槽接口 Java中提供了两套API,UDP一套,TCP一套
TCP有连接,可靠传输,面向字节流,全双工 UDP无连接,不可靠传输,面向数据报,全双工
通过代码直接操作网卡,不好操作(网卡有很多种不同的型号,之间提供的api都会有差别) 操作系统就把网卡概念封装成socket.应用程序员不必关注硬件的差异和细节,统一去操作socket对象,就能间接的操作网卡. socket就像遥控器一样, 从socket里读数据就相当于接受网卡传来的网络信号(数据流向:网卡 → 内核→ Socket), 往socket里写数据相当于控制网卡发送网络信号(数据流向:Socket → 内核 → 网卡) Socket 与遥控器的类比逻辑 类比对象遥控器Socket核心功能远程控制设备(如电视、空调)远程控制网络数据收发操作抽象按下按钮(抽象操作)→ 遥控器内部电路处理 → 红外信号发射调用 Socket 接口(如
send()/recv())→ 操作系统内核处理 → 网卡硬件收发数据用户感知无需关心红外信号的物理特性无需关心网卡驱动、TCP/IP 协议细节 2.1DatagramSocket
写一个最简单的客户端服务器程序,不涉及到业务流程只是对于api的用法做演示 回显服务器"(echo server) 客户端发啥样的请求,服务器就返回啥样的响应 没有任何业务逻辑,进行任何计算或者处理


对于一个系统来说,同一时刻,一个端口号,只能被一个进程绑定 但是一个进程可以绑定多个端口号(通过创建多个socket对象来完成)

requestPacket.getSocketAddress():获取请求方的完整地址(IP + 端口)

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
//在构造函数中指定端口号,使服务器绑定到该端口
public UdpEchoServer (int port) throws SocketException {
socket = new DatagramSocket(port);
}
public String process (String request){
return request;
}
//通过start启动服务器的核心流程
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
//通过死循环不停处理客户端请求
//1.读取客户端的请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//上述收到的数据是二进制byte[]的形式体现的,后续若需打印需转换为字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应,由于此处是回显服务器,响应就是请求
String response = process(request);
//3.把响应写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d] req=%s,resp=%s\n", requestPacket.getAddress(),requestPacket.getPort(),
request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}

request.getBytes() 方法将字符串 request 转换为字节数组。由于网络传输的基本单位是字节,而 UDP 数据包需要填充字节形式的数据,因此必须进行此转换。
InetAddress.getByName(this.serverIP)将字符串形式的 IP 地址或域名,解析成 Java 能识别的 InetAddress 对象,即将“服务器地址” 翻译成 Java 程序能听懂的 “网络地址语言”


import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
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 scanner = new Scanner(System.in);
while(true) {
//1.从控制台读取到用户的输入
System.out.println("->");
String request = scanner.next();
//2.构造出一个UDP请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIP),this.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 client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}










ServerSocket 是创建TCP服务端Socket的API,专门给服务器使用的socket对象 ServerSocket构造方法 方法签名方法说明ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口 ServerSocket方法 方法签名方法说明Socket accept()开始监听指定端口(创建时绑定的端口),有客户端 连接后,返回一个服务端Socket对象,并基于该 Socket建立与客户端的连接,否则阻塞等待void close()关闭此套接字 TCP是有连接的,有连接就需要有一个“建立连接”的过程 建立连接的过程就类似于打电话,此处的accept就相当于"接电话" 由于客户端是"主动发起”的一方,服务器是"被动接受”的一方,所以客户端打电话,服务器接电话
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回服务端的Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。 即会给客户端使用也会给服务器使用 Socket构造方法 方法签名方法说明Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 构造这个对象,就是和服务器“打电话”“建立连接” Socket方法 方法签名方法说明InetAddress getInetAddress()返回套接字所连接的地址InputStream getInputStream() 返回此套接字的输入流OutputStream getOutputStream()返回此套接字的输出流
同一个协议下,一个端口只能被一个进程绑定,9090端口在UDP下被一个进程绑定了,还可以在TCP下被另一个进程绑定 不能绑定多个UDP或TCP 如图表示绑定的端口号已被占用,此时就需要找找,端口是被谁绑定了,找到对应的进程 决定是结束旧的进程,还是修改新进程的端口




TCP是全双工通信,所以既可以读也可以写,inputStream outputStream
Scanner 包装 InputStream,将字节流转换为字符流来读取请求数据(字节->字符) PrintWriter 处理 OutputStream,将文本数据以字符流形式写入输出流,发送给对方 (字符->字节)

写入响应的时候末尾加上\n,TCP是字节流的.读写方式存在无数种可能,就需要有办法区分出,从哪里到哪里是一个完整的请求数据,此处就可以引入分隔符来区分

读取数据时就隐藏了条件,请求应以空白符(空格,回车,制表符等)结尾

因此此处就约定,使用\n作为请求和响应的结尾标志 后续客户端,也会使用scanner.next读取响应
线程池的创建是为了复用线程,解决了客户端发一个请求之后就快速断开连接了
ExecutorService service = Executors.newCachedThreadPool();客户端持续的发送请求处理响应,连接会保持很久~~
服务器的两处阻塞
等待客户端连上
//客户端
socket = new Socket(serverIP,port);
//服务器
Socket clientSocket = serverSocket.accept();等待客户端发送数据
//客户端,把请求发送给服务器
printWriter.println(request);
//服务器
if(!scanner.hasNext()) {
//如果scanner无法读取数据,说明客户端关闭了连接,导致服务器这边读取到了“末尾”
break;
}容易出现的三个bug
服务器发送了数据之后,并没有任何响应,此处的情况是客户端并没有真正把请求发送出去
printWriter.println(request);printWriter这个类“自带缓冲区”,把请求先放到内存的缓冲区里,由于此处数据比较少,因此这样的数据就一直停留在缓冲区中无法进行发送 引入缓冲区之后,进行写入数据的操作,不会立即触发1O,而是先放到内存缓冲 区中,等到缓冲区里攒了一波之后,再统一进行发送 因此我们需要引入flush操作“刷新缓冲区”
printWriter.flush();针对cilentSocket没有进行close操作 像ServerSocket,DatagramSocket他们的生命周期都是跟随整个进程的,而此处的此cilentSocket是“连接级别”的数据,随着客户端断开连接了,这个socket也就不再使用了,即使是同一个客户端断开之后重新连接socket也是不同的,因此这样的socket需要主动关闭,否则就会造成文件资源泄露
clientSocket.close();只能为一个客户端提供服务,应该满足同时给多个客户端提供服务 当第一个客户端连上服务器之后,服务器代码就会进入processConnect内部的while循环,此时第二个客户端尝试连接时,无法执行到第二次accept,所以第二个客户端发来的请求数据都积压在操作系统的接收缓冲区中 此处无法处理多个客户端,本质上是服务器代码结构存在问题,采取了双重while循环的写法.就会导致,进入里层while的时候,外层while无法继续执行了,此时我们只需要改为一层循环分别进行执行即可,用多线程来实现, 主线程是accept,每个客户端连接由独立线程处理,主线程继续接收下一个连接。
public void start() throws IOException {
System.out.println("启动服务器");
//System.err.println("启动服务器");
//线程池
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
Socket clientSocket = serverSocket.accept();
service.submit(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}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 {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
System.err.println("启动服务器");
//线程池
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
Socket clientSocket = serverSocket.accept();
service.submit(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
//针对一个连接提供处理逻辑
private void processConnection(Socket clientSocket) throws IOException {
//先打印一下客户端的信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//获取到 socket 中持有的流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//使用scanner包装一下inputStream就可以更方便的读取这里的请求数据了
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true) {
//1.读取请求并解析
if(!scanner.hasNext()) {
//如果scanner无法读取数据,说明客户端关闭了连接,导致服务器这边读取到了“末尾”
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端
//此处可以按照字节数组直接来写,也可以有另外一种写法
//outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}对象一new好就会和服务器建好连接,服务器accept如果建立连接失败就会在构造对象时抛出异常
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}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;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取数据
System.out.print("-> ");
String request = scannerIn.next();
// 2. 把请求发送给服务器
printWriter.println(request);
printWriter.flush();
// 3. 从服务器读取响应
if (!scanner.hasNext()) {
break;
}
String response = scanner.next();
// 4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}