①Socket套接字:Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。基于Socket套接字的⽹络程序开发就是⽹络编程。
Socket套接字主要针对传输层协议划分为如下三类:
(1)数据报套接字:使⽤传输层UDP协议
UDP,即UserDatagramProtocol(⽤⼾数据报协议),传输层协议。
以下为UDP的特点(细节后续再学习):
• ⽆连接
• 不可靠传输
• ⾯向数据报
• 有接收缓冲区,⽆发送缓冲区
• ⼤⼩受限:⼀次最多传输64k
(2)流套接字:使⽤传输层TCP协议
以下为TCP的特点:
• 有连接
• 可靠传输
• ⾯向字节流
• 有接收缓冲区,也有发送缓冲区
• ⼤⼩不限
(3)原始套接字
②Java数据报套接字通信基础代码:
服务器端:
java
import javax.xml.crypto.Data;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.URLDecoder;
public class demo15UdpEchoServer {
private DatagramSocket socket=null;
public demo15UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
(1)DatagramSocket:UDP 通信的插座 / 通道 ,负责接收 / 发送 UDP 数据包。先定义但不初始化
(2)构造方法:输入端口号(还可以写ip,一般写端口号就足够了,也是最标准、最简单的写法)
java
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
DatagramPacket requestPacket =new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
String request =new String(requestPacket.getData(),0,requestPacket.getLength());
String response=process(request);
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//此处不能用response.length()
socket.send(responsePacket);//需要指定目的ip目的端口
System.out.printf("[%s:%d] req:%s, resp:%s\n",requestPacket.getAddress().toString(),responsePacket.getPort(),request,response);
//打印日志
}
}
(1)start方法就是服务器主体代码,用while(true)模拟服务器一直运行的状态
(2)DatagramPacket:创建一个长度为 4096 的字节数组用于接收数据,但是只能接收一次!超出的长度直接舍弃且不会报错,但是4096足够应付绝大多数 UDP 场景
(3)socket.receive用于把数据内容放进 requestPacket 的 byte[] 里
(4)String request:把数据包中的数据转为String类型,然后长度是数据包中byte数组的从0开始往后已存入的长度个,否则如果没填满数组会把一堆空字节也传入String
(5)String response=process(request);用来处理转为字符串类型的byte然后用新的resopnse字符串接受
(6)DatagramPacket responsePacket:再次创建一个DatagramPacket用来存放处理完的结果(respoese),然后发出也要用byte模式,长度也要用byte形式的长度所以是response.getBytes().length);然后requestPacket.getSocketAddress()用来指定发送目标
socket.receive(requestPacket)执行完之后,系统自动在DatagramSocket socket里储存了地址,直接拿就行!(在 requestPacket.getSocketAddress()中**)**第一次主动发给别人 → 必须手动写 IP(或者 getInetAddress(),这个只包含ip**) + 端口**
接收数据之后,服务器可以直接用getSocketAddress()既包含ip也包含端口
++接收包:只需要空箱子 → 传 byte [] + 最大长度++
++发送包:必须装满货 + 写地址 → 传 数据 + 长度 + 地址(++ ip+端口 ++)++
(7)socket.send(responsePacket),就是利用socket把处理完的数据包responsePacket发送出去
java
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
demo15UdpEchoServer server=new demo15UdpEchoServer(9090);
server.start();
}
}
(1)process方法,模拟处理,这里根据需求写返回String即可。这里直接返回一样的用来模拟
(2)mian方法里:demo15UdpEchoServer server=new demo15UdpEchoServer(9090);
因为所有方法都是直接在demo15UdpEchoServer服务器类里psvm外,而且也没有构造多的类来实现,所以实例化demo15UdpEchoServer
(3)server.start(); :用于启动服务器
客户端:
java
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class demo16UDPEchoClient {
private DatagramSocket socket=null;
private String serverIp;
private int serverPort;
//UDP本身不保存对端信息,自己代码保存
public demo16UDPEchoClient(String serverIp,int serverPort) throws SocketException {
this.serverIp=serverIp;
this.serverPort=serverPort;
socket=new DatagramSocket();//不能把serverPort填这里面 客户端一定随机分配端口
}
(1)DatagramSocket socket=null;用于连接服务器(发送数据)、
(2)private String serverIp;/private int serverPort;储存服务器的ip和端口
(3)demo16UDPEchoClient构造方法:输入服务器的ip和端口,然后实例化socket
java
public void start() throws IOException {
while (true) {
Scanner cin = new Scanner(System.in);
System.out.println("请输入要发送的内容");
String request = cin.next();
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
DatagramPacket responsePacket = new DatagramPacket(new byte[4086], 4086);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());//开始构造的位置,长度(responsePacket的吗)
System.out.println(response);
}
}
(1)一样的start方法,一直while(true)用来反复等待客户操作
(2)DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
这个就是用户输入的数据包,把输入的String类型的转成byte,和bytes的长度,然后输入服务器的ip和端口(跟服务器端的requestPacket.getSocketAddress()一样)
InetAddress.getByName(serverIp):把字符串格式的 IP(如 "127.0.0.1")变成 Java 能识别的 IP 对象。
(3)socket.send(requestPacket);利用socket把数据包传出去
(4)DatagramPacket responsePacket = new DatagramPacket(new byte[4086], 4086);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
跟服务器一样的接收方法
(5)System.out.println(response);输出服务器返回的处理结果
java
public static void main(String[] args) throws IOException {
demo16UDPEchoClient client=new demo16UDPEchoClient("127.0.0.1",9090);
client.start();
}
}
(1)方法都写在psvm外,public类里,所以要实例化
(2)跟服务器一样都要调用start
process处理案例:输入中文返回对应英文
java
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class demo17UdpDictServer extends demo15UdpEchoServer{
private HashMap<String,String> dict=new HashMap<>();
public demo17UdpDictServer(int port) throws SocketException {
super(port);
dict.put("小猫","cat");
dict.put("小狗","dog");
dict.put("兔子","rabbit");
dict.put("鸭子","duck");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"未找到该词条");
}
public static void main(String[] args) throws IOException {
demo17UdpDictServer server=new demo17UdpDictServer(9090);
server.start();
}
}
(1)使用HashMap:根据一个词,快速找到另一个词。HashMap 就是 Java 里专门干「快速查找」这件事的。/
(2)构造方法:其实是为了传数据给继承的父类
(这里只是为了方便继承父类演示一下处理问题的过程,实际上修改process方法即可)
③TCP流套接字编程
服务器端:
java
import java.awt.*;
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 demo18TcpEchoServer {
private ServerSocket serverSocket=null;
public demo18TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
Socket clientSocket=serverSocket.accept();
pool.submit(()->{
processConnection(clientSocket);
});
}
}
(1)ServerSocket :用于服务器端,等待客户端连接(客户端用Socket连接),定义但不初始化
(2)构造方法:传入端口,然后初始化ServerSocket的时候要传入端口
客户端要指定 (服务器的)IP + 端口,服务端只需要指定端口。
start方法:
(3)使用多线程:如果不用多线程,那么while循环里processConnection(clientSocket);的时候,因为processConnection(clientSocket);里面还有一个while循环是出不来的,所以只能第一个连接的客户端输入后得到输出,后面的客户端连接的时候由于不是多线程所以无法得到反馈
(4)使用newCachedThreadPool线程池:因为服务器不知道会来多少个客户端,CachedThreadPool 会自动创建线程、自动回收,最适合【短连接、客户端数量不固定】的网络服务。
(5)Socket clientSocket=serverSocket.accept();用于等待一个客户端来连接;没有客户端连接的时候,这行代码会一直阻塞(卡住不动)
这个接受代码放在循环里是可以接收完一个就接收第二个客户端的,但是也是因为如果没有多线程的话,while循环就无法进行到第二次这个代码所以也无法接受第二个客户端
java
private void processConnection(Socket clientSocket) {
System.out.println("["+clientSocket.getInetAddress()+":"+clientSocket.getPort()+"]"+"客户端上线!");
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
Scanner cin=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while(true){
if(!cin.hasNext()) {//没有下一个数据可读了
System.out.println("["+clientSocket.getInetAddress()+":"+clientSocket.getPort()+"]"+"客户端下线!");
break;
}
String request=cin.next();
String response=process(request);
writer.println(response);
writer.flush();
System.out.println("["+clientSocket.getInetAddress()+":"+clientSocket.getPort()+"] req:"+request+", resp:"+response);
}
}catch (IOException e){
throw new RuntimeException(e);
}finally {
try {
clientSocket.close();
}catch (IOException e){
throw new RuntimeException(e);
}
}
}
接受调用处理并返回模块
(1)System.out.println("["+clientSocket.getInetAddress()+":"+clientSocket.getPort()+"]"+"客户端上线!");
用于提示服务器客户端上线。clientSocket.getInetAddress提供客户端的ip,clientSocket.getPort()提供客户端端口
(2)
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
InputStream 用来读客户端发来的消息; OutputStream 用来写给客户端的消息。注意之前是new File... 这里是直接用clientSocket对应的Input和Output的api
为什么放在try-catch代码块里?:因为流、Socket 这些 IO 操作随时可能出错,Java 强制要求必须捕获异常,不写 try-catch 编译都过不去
(3)Scanner cin=new Scanner(inputStream); 把字节流包装成扫描器,方便读取字符串
PrintWriter writer=new PrintWriter(outputStream); :包装输出流,方便打印字符串
就是把刚刚try里面的实例名,输入放进Scanner定义的括号里;输出放进PrintWriter里
客户端和服务器之间,只传 0101 的字节,不能直接传字符串。
所以我们需要两个 "翻译工具":
Scanner 把字节 → 字符串
PrintWriter 把字符串 → 字节
(4)while循环: 只要客户端不断开,我就一直等着收消息、回消息。
(5)if(!cin.hasNext()) { : 只需要记住客户端的输出流关闭了 → 客户端断开连接了,才会出发这个即可
(6)String request = cin.next(); 这里的cin并不是真实客户端输入,而是InputStream inputStream=clientSocket.getInputStream()转化过来的字符串(这里的cin已经把二进制转为字符串)
String response=process(request); : 用于处理数据并且用response来获取返回的字符writer.println(response); : 跟cin同理,把字符串 转成 二进制,发送给客户端
writer.flush(); 用于
writer会自动帮你:字符串 → 二进制
println(...)把转换后的二进制,通过输出流发给客户端
outputStream= 服务器 → 客户端 的网络管道,你把 writer 绑在了这个网络管道上,所以println直接返回客户端
(7)System.out.println("["+clientSocket.getInetAddress()+":"+clientSocket.getPort()+"] req:"+request+", resp:"+response);
服务器面板输出用于记录输入和返回的字符串
(8)clientSocket.close(); 挂断连接 + 释放资源,在while循环结束(即客户端下线时)触发
java
private String process(String request){
return request;//代替处理
}
public static void main(String[] args) throws IOException {
demo18TcpEchoServer server=new demo18TcpEchoServer(9090);
server.start();
}
}
(1)process方法:用于处理客户端发送的字符串
(2)psvm:实例化(设置端口)+启动服务器
客户端:
java
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 demo19TcpEchoClient {
private Socket socket=null;
public demo19TcpEchoClient(String serverIp,int serverPort) throws IOException {
socket=new Socket(serverIp,serverPort);
}
(1)定义Socket但不初始化,用于连接客户端
(2)构造方法:输入要连接的服务器的ip 和服务器设置的端口,然后初始化socket的时候传入ip和端口
java
public void start() {
Scanner cin = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//为了使用方便,套壳
Scanner cinNet=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while (true) {
String request = cin.next();
writer.println(request);//发送给服务器
writer.flush();//加上刷新缓冲器操作才能真正发出去
String response=cinNet.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
(1)正常定义Scanner用于输入
(2)try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner cinNet=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
跟服务器一样,inputSteam用于接收,用Scanner包装
outputStream用于输出,用PrintWriter包装
(3)while(true)循环:让客户端一直能发消息、收消息,直到你手动关闭程序
(4) String request = cin.next(); 用于接收字符串
writer.println(request); : 跟服务器同理,用于发送给服务器
writer.flush();: 把缓存区里的消息,强制立刻发送 给服务器 (只需要记住只要用 PrintWrite,后面必须跟一句 flush())
java
public static void main(String[] args) throws IOException {
demo19TcpEchoClient client=new demo19TcpEchoClient("127.0.0.1",9090);
client.start();
}
(1)实例化输入服务器ip(个人电脑服务器就是本机,用127.0.0.1代替本机)和端口 + 启动客户端