Socket套接字,是由系统提供用于网络通信的技术(操作系统给应用程序提供的一组API叫做Socket API),是基于TCP/IP
协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
socket可以视为是应用层和传输层之间的通信桥梁;
传输层的核心协议有两种:TCP,UDP
;socket API也有对应的两组,由于TCP和UDP协议差别很大,因此,这两组API差别也挺大。
分类:
Socket
套接字主要针对传输层协议划分为如下三类:
TCP的特点:
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
UDP的特点:
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
UDPSocket
中,主要涉及到两类:DatagramSocket、DatagramPacket
;
DatagramSocket
创建了一个UDP
版本的Socket
对象,用于发送和接收UDP数据报,代表着操作系统中的一个socket文件,(操作系统实现的功能–>)代表着网卡硬件设备的抽象体现。
DatagramSocket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
代表了一个UDP数据报,是UDP Socket发送和接收的数据报,每次发送/接收数据报,都是在传输一个DatagramPacket
对象。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
构造UDP
发送的数据报时,需要传入 SocketAddress
,该对象可以使用 InetSocketAddress
来创建。
InetSocketAddress
( SocketAddress
的子类 )构造方法:
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
EchoSever
)多个进程不能绑定同一个端口号,但是一个进程可以绑定多个端口
,(这就好比一个人可以拥有多个手机号),一个进程可以创建多个Socket对象,每个Socket都绑定自己的端口。
这个长度不一定是1024,假设这里的UDP数据最长是1024,实际的数据可能不够1024.
这里的参数不再是一个空的字节数组了,response是刚才根据请求计算的得到的响应,是非空的,DatagramPacket
里面的数据就是String response的数据。
response.getBytes().length
:这里拿到的是字节数组的长度(字节的个数),而response.length得到的是字符的长度。
一次通信是由5个核心信息描述的:源IP、 源端口、 目的IP、 目的端口、 协议类型。
站在客户端角度:
站在服务器的角度:
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//创建一个Socket实例
private DatagramSocket socket = null;
//port 端口号在运行程序的时候手动指定
public UdpEchoServer(int port) throws SocketException {
//需要显示的绑定一个端口号
//端口号是用来区分一个应用程序的,主机收到网卡上数据的时候,这个数据应该给哪个程序?
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("启动服务器!");
//UDP不需要建立连接,直接接收从客户端来的数据
while (true){
//1.读取客户端发来的请求
DatagramPacket datagramPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(datagramPacket);//为了接收数据,需要准备好datagramPacket对象,由receive来进行接收,这里的datagramPacket为输出型参数
//将datagramPacket解析成String
String request = new String(datagramPacket.getData(),0,datagramPacket.getLength(),"UTF-8");
//2.根据请求计算响应(由于这里是回显服务,所以2省略)
String response = process(request);
//3.把响应写回到客户端
//第一个参数不再是一个空的字节数,第三个参数:表示要把数据发给哪个端口+地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,datagramPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req :%s,resp: %s\n",responsePacket.getAddress().toString(),
responsePacket.getPort(),request,response);
}
}
//由于是回显服务,响应就和请求一样
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer sever = new UdpEchoServer(9090);
sever.start();
}
}
这里就是系统自动给客户端分配的端口;
客户端可以有很多的,一个服务器可以给很多客户端提供服务;
package UDP;
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 severIP;
private int severPort;
//客户端构造socket对象时不再手动指定端口号,自己分配一个空闲的端口号
// port是服务器的端口,客户端启动的时候不需要给socket指定端口,客户端自己的端口是系统随机指定的
public UdpEchoClient(String ip,int port) throws SocketException {
socket = new DatagramSocket();
severIP = ip;
severPort = port;
}
public void start() throws IOException {
//1.从控制台读取用户的字符串
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("-> ");
String request = sc.next();
//2.把这个用户输入的内容,构造成一个UDP请求,并发送
//构造的请求里面包含两部分信息:1.数据的内容request字符串。 2.数据要发给谁:服务器的IP+端口
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(severIP),severPort);
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是一样的
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
客户端不变,服务器代码进行调整:主要是调整process方法;
读取请求并解析,把响应写回到客户端,这两步是一样的,关键逻辑就是:根据请求处理响应。
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("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();
}
}
TCP API 也涉及到两个核心的类:
ServerSocket
:专门给TCP服务器用;
Socket
:即需要给服务器用,又需要给客户端用;
主要通过这样的类,来描述一个socket文件即可,而不需要专门的类表示"传输的包";面向字节流(以字节为单位传输的)。
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连 |
Socket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回该套接字的输入流 |
OutputStream getOutputStream() | 返回该套接字的输出流 |
这里之所以分成了两步,就是因为要建立连接.一个专门负责建立连接,一个专门负责数据通信。
package UDP;
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 {
// 操作系统原生的 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());
//处理请求和响应
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()){
//循环处理每个请求,分别返回响应
Scanner scanner = new Scanner(System.in);
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, reps %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 tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
对于UDP的socket来说,构造方法指定的端口,表示自己绑定哪个端口;
对于TCP的ServerSocket来说构造方法指定的端口,也是表示自己绑定哪个端口;
对于TCP的Socket来说构造方法指定的端口,表示要连接的服务器的端口.
UDP没有用多线程因为:
UDP不需要处理连接,UDP只要一个循环,就可以处理所有客户端的请求.但是此处, TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环.里层循环,就会影响到外层循环的进度了.
主线程,循环调用accept .当有客户端连接上来的时候,就直接让主线程创建一个新线程。由新线程负责对客户端的若干个请求,提供服务.(在新线程里,通过while循环来处理请求).这个时候,多个线程是并发执行的关系(宏观上看起来同时执行),就是各自执行各自的了,就不会相互干扰(也要注意:每个客户端连上来都得分配一个线程)
多线程版本,可以同时与多个客户端进行通信:
package UDP;
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 TcpThreadEchoSever {
// 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadEchoSever(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成功,都创建一个新的线程,由新线程负责执行clientSocket方法
Thread t = new Thread(()->{
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//处理请求和响应
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()){
//循环处理每个请求,分别返回响应
Scanner scanner = new Scanner(System.in);
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, reps %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 {
TcpThreadEchoSever tcpThreadEchoSever = new TcpThreadEchoSever(9090);
tcpThreadEchoSever.start();
}
}
TCP服务器线程池版本:
package UDP;
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 TcpThreadPollEchoServer {
// 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
// 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpThreadPollEchoServer(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 {
TcpThreadPollEchoServer server = new TcpThreadPollEchoServer(9090);
server.start();
}
}
package UDP;
import java.io.IOException;
import java.util.HashMap;
public class TcpDictSever extends TcpThreadPollEchoServer {
private HashMap<String,String> map = new HashMap<>();
public TcpDictSever(int port) throws IOException {
super(port);
map.put("cat","猫");
map.put("pig","猪");
map.put("dog","狗");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"当前词组无法找到!");
}
public static void main(String[] args) throws IOException {
TcpThreadPollEchoServer server = new TcpThreadPollEchoServer(9090);
server.start();
}
}
以上。