目录
什么是Socket ?
TCP api 与 UDP api 的特点 :
UDP api
使用UDP Socket 实现一个单词翻译 :
TCP api
使用TCP协议来实现一个回显服务
什么是Socket ?
应用层和传输层之间的桥梁 .
程序猿写网络代码 (应用层) , 要想发送这个数据 , 就需要去调用下层协议 , 应用层就要去调用传输层. 传输层给应用层提供了一组 api . 称为 Socket api ;
Socket api 有两组 : 一组是基于UDP 的 api , 一组是基于 TCP 的api.
TCP api 与 UDP api 的特点 :
TCP api 与 UDP api 的特点 :
UDP : 无连接 , 不可靠传输 , 面向数据包 , 全双工 .
举个栗子 :
无连接 : 就好比是发短信 , 我只用知道对方的手机号 , 就可以给他发送消息了. 这就是无连接,通信前 是不用建立连接的. (使用UDP通信的双方,不需要刻意保存对端的相关信息)
不可靠传输 : 就好比我们发的短信 , 短信发出去了 , 至于对方看没看见 , 对我来说不重要了.
面向数据报 : 在UDP 中 , 每次发送都是以一个数据报作为一个发送的单位.
TCP : 有连接 , 可靠传输 , 面向字节流 , 全双工 .
举个栗子 :
有连接 : 就好比打电话 , 我想跟对方交流就要先进行建立连接 , 等对方一接 , 我们就可以通话了 , (使用TCP 通信的双方 , 则需要刻意保存对方的相关信息).
可靠传输 : 对方接了电话之后 , 证明我们之间有连接了 , 一般我们都会试探性的问一句"能听到吗?" , 等对方说"能听到" , 表示我们说的话对方可以收到. (因此可靠传输指的就是消息传输出去后我们可以知道对方有没有收到).(但是我们建立连接后 , 不能够保证对方100%能收到)
面向字节流 : 以字节为传输的基本单位 , 读写方式非常灵活/方便.
单双工 与 全双工 的区别 :
单双工 : 就像是一个吸管 , 只能一边进气 , 另一边出气.
全双工 : 就像是马路 , 在同一条马路上 , 汽车和人 , 都可以朝着相反的方向移动.
UDP api
UDP api 主要有两个类 : DatagramSocket , DatagramPacket .
由 DatagramSocket 创建出来的对象就是一个socket 对象 .
那什么是scoket对象呢 ?
socket 对象 对应到网卡这个硬件设备!
把socket 想象成为一个遥控器 , 这个遥控器控制的就是网卡.
它是一个特殊的文件(socket文件).
我们对socket 对象进行读操作 , 那么就是在读网卡.
对socket 对象进行写操作 , 那么就是在进行写网卡.
由DatagramPacket 创建出来的对象就是一个UDP数据包 .
使用UDP api 进行读写操作时 , 每次就只能以数据包作为一个单位.
DatagramSocket 是UDP Socket , 用于发送和接收UDP 数据报.
DatagramSocket 两个常用的构造方法 : 一个是无参构造 , 一个是带有一个参数的构造方法.
两个方法都是用来给Socket 对象来绑定一个 端口号.
无参构造是系统自动来分配一个空闲的端口号.
带有一个参数的构造 : 自己主动来绑定一个端口号.
此处的Socket 对象要被客户端 / 服务器 都使用的 . 对于服务器来说 Socket 对象往往都要绑定一个具体的端口号(自己主动绑定) . 客户端这边则不需要手动指定 , 让系统自动分配即可.
这么做的好处 : 因为服务器它是用来处理客户端的请求的 , 因此它就要有一个明确的端口号,让客户端可以找到 . 对于客户端来说 , 属于主动发送请求的一方 , 服务器实现不需要知道客户端的端口号 , 因此让系统自动分配端口号给客户端.
不手动分配端口号给客户端是因为 : 我们不确定当前给客户端分配的这个端口号是不是已经被其他进程占用了 . 这样就会很麻烦 , 让系统自动分配会更好.
DatagramSocket 常见的方法 :
重点关注一下 close() 方法 , Socket 它也是一个文件 , 文件用完了就要记得关闭 , 否则就会出现文件资源泄露的问题!!!!!
DatagramPacket API
构造方法 :
带有 SocketAddress 的构造方法 : 需要显式的设置地址 , 通常用来发送消息.
方法 :
使用UDP Socket 实现一个单词翻译 :
服务器 :
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
// 服务器
public class UdpTraServer {
// 创建一个Socket 对象
DatagramSocket socket = null;
// 给服务器指定一个端口号
public UdpTraServer(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器已启动!");
while (true) {
// 1. 接收来自客户端的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
// 将请求转变为String类型能够更方便的去处理.
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()); // 这里要指定客户端的IP地址和端口号
// 4. 发送给客户端
socket.send(responsePacket);
System.out.printf("[%s : %d] 请求: %s 响应 : %s \n",requestPacket.getAddress(),requestPacket.getPort(),request,response);
}
}
private String process(String request) {
HashMap<String,String> hash = new HashMap<>();
hash.put("one","1");
hash.put("two","2");
hash.put("three","3");
hash.put("four","4");
return hash.getOrDefault(request,"null");
}
public static void main(String[] args) throws IOException {
// 给服务器指定一个9090的端口号.
UdpTraServer udpTraServer = new UdpTraServer(9090);
udpTraServer.start();
}
}
客户端 :
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpTraClient {
// 创建一个Socket 对象来进行网络编程.
DatagramSocket socket = null;
String serverIP = null;
int port = 0;
// 通过构造方法来指定服务器的IP地址和端口号
public UdpTraClient(String serverIP,int port) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.port = port;
}
public void start() throws IOException {
Scanner input = new Scanner(System.in);
while (true) {
System.out.println("----> ");
// 1. 从键盘上输入请求
String request = input.nextLine();
// 2. 将请求打包成数据包
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),port);
// 3. 发送请求
socket.send(requestPacket);
// 4. 接收请求
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
// 将请求转变为String 进行输出
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
System.out.printf("请求 : %s 响应 : %s \n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpTraClient udpTraClient = new UdpTraClient("127.0.0.1",9090);
udpTraClient.start();
}
}
注意在进行 receive 和 send 操作时 , 在等待数据时会处在阻塞状态 !
TCP api
面向字节流进行网络编程 .
TCP 中主要的核心类有两个 :
ServerSocket : 专门给服务器提供的api , ServerSocket 主要做的事情就是负责和客户端建立连接.
构造方法 :
创建服务器时 , 要指定一个端口号 .
Socket : 通过Socket对象来进行服务器和客户端之间的通信 !
TCP的最最最核心 : 可靠性
当客户端主动向服务器发起连接请求 , 服务器就得同意一下. 表示服务器和客户端已经连接成功了 , 可以进行交互了 .(但是实际上这个accept , 其实tcp连接的接受是在内核里已经完成了 , 这里的accept是应用层面的接受)
通过Socket中的 getInputStream 和 getOutputStream 两个方法 , 可以进行对数据的输入和输出操作了 .
从InputStream 这里读数据就相当于从网卡接收 , 往OutputStream 这里写数据,相当于从网卡发送.
使用TCP协议来实现一个回显服务
约定 : 1. 每个请求是一个字符串(文本数据)
2. 请求和请求之间 , 使用 \n 来分割
服务器 :
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 {
ServerSocket serverSocket = null;
// 定义构造方法 , 为服务器设置一个端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器已启动!");
// 因为服务器要处理多个客户端的请求 , 套上一个循环
while (true) {
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private static void processConnection(Socket clientSocket) {
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 方便对字节流进行读写操作 创建 Scanner 和 PrintWriter
Scanner scannerFromSocket = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 读取请求
if (!scannerFromSocket.hasNext()) {
// 证明读取到流的末尾了 (对端的客户端已经关闭了)
System.out.printf("[%s : %d] 客户端已经下线! \n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
// 通过scannerFromSocket 对象来读取请求
String request = scannerFromSocket.nextLine();
// 处理请求
String response = process(request);
// 将请求写回客户端
// 注意在这里写回的时候 使用的println 自带的换行操作
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();
}
}
}
// 由于这里是回显服务 , 所以直接返回这个原请求就可以了
private static String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客户端 :
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 {
public Socket socket = null;
// 定义一个构造方法 给出要连接的端口号 , 和 IP地址
public TcpEchoClient(int port, String ServerIp) throws IOException {
// 重点注意 : 这个操作相当于让客户端和服务器建立TCP连接
// 这里如果连接上了 , 服务器的 accept 就会返回
socket = new Socket(ServerIp,port);
}
public void start() {
Scanner input = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 使用Scanner 和 PrintWrite 来对字节流进行输入和输出
Scanner scannerFromSocket = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
System.out.println("----> ");
// 1. 得到请求
String request = input.nextLine();
// 2 把读取到的内容构造成请求 , 发送给服务器
// 注意, 由于我们之前约定好 , 每个请求之间是由\n 来区分
// 使用printWriter 的 println 就会自动带上换行
printWriter.println(request);
// 刷新缓冲区
printWriter.flush();
// 3. 读取响应
// 通过scannerFromSocket
String response = scannerFromSocket.nextLine();
// 4. 把响应结果显示到控制台上
System.out.printf("req : %s , resp : %s \n",request,response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient(9090,"127.0.0.1");
tcpEchoClient.start();
}
}
以上代码的思路 :
先开启服务器 , 再开启客户端.
1 . 服务器先运行start , 阻塞在accept .
2 . 客户端启动时 , 根据端口和IP地址 , 创建出Socket对象 , 同时和服务器建立连接 , 连接成功后 , 服务器的accept就会返回.
3 . 服务器和客户端会同时往下执行 , 客户端往下执行到从控制台读取用户输入也会阻塞. 而服务器就进入processConection方法 , 尝试从客户端读取请求 , 如果此时客户端还没有发送请求 , 那么读取操作也会阻塞.
4 . 当用户从控制台上输入内容后 , 客户端进行发送请求 , 同时往下执行 , 读取服务器的响应 , 再次进行阻塞 .
5 . 服务器收到客户端的请求后 , 从scanner 对象中读出 , 在process方法中进行处理响应 , 通过println方法将响应返回给客户端. 紧接着服务器就会回到等待请求的阻塞状态.
6 . 客户端通过scannerFromSocket 对象读取到响应结果后 , 将结果进行打印 , 然后再次从控制台上等待用户的输入了.
刷新缓冲区 :
无论是客户端将请求发送给服务器还是服务器将响应返回给客户端.都要进行一个刷新缓冲区的操作.
原因 : 在于速度 , 内存的速度比硬盘要快上几千倍 , 而网卡的速度大概率还比硬盘都要慢 .
对于上述我们进行的网络编程都是在对网卡进行读写操作.
对于提高IO的效率 (读写网卡 , 读写网卡都可以视为IO操作) 引入了缓冲区 , 使用缓冲区减少了IO次数 , 就可以提高整体的效率 , 例如 : 如果要进行多次写操作 , 就先把要写的数据放到一个内存构成了缓冲区中 , 在统一把这个缓冲区中的数据写入网卡中.
因此我们就要手动去刷新缓冲区 , 为了让对端第一时间拿到数据.
当客户端断开连接后 , 服务器要进行关闭连接操作 , 否则可能会造成文件泄露 , 不关闭文件就会一直被服务器占用着 , 而内存是有限的 , 达到一定程度后, 服务器可能就崩了.
设计服务器的初心是给多个客户端进行提供服务的 , 但是上述的TCP协议实现的服务器 , 它一次只能服务一个客户端 .
TCP的前提是要先建立连接 , 上面的代码 , 如果第一个客户端没有结束 , 就会导致processConnection这个方法不会结束, 从而就会导致服务器不会执行到accept这个方法 , 再去接收其他客户端的请求了 , 解决方案就是多线程.
public void start() throws IOException {
System.out.println("服务器已启动!");
// 因为服务器要处理多个客户端的请求 , 套上一个循环
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
或者使用线程池
public void start() throws IOException {
System.out.println("服务器已启动!");
// 因为服务器要处理多个客户端的请求 , 套上一个循环
while (true) {
ExecutorService pool = Executors.newCachedThreadPool();
Socket clientSocket = serverSocket.accept();
pool.submit(() -> {
processConnection(clientSocket);
});
}
}
TCP区别于UDP :
UDP版本的程序就不需要多线程 , 也可以处理客户端的请求 , 原因在于UDP不需要处理连接 , UDP 只要一个循环 , 就可以处理所有客户端的请求了.