Socket的概念
Socket英文是插座的意思,使用该英文来体现网络通信可以说很生动形象。 Socket是由操作系统提供用于网络编程的一组API,是基于TCP/IP协议网络通信的基本操作单元。socket本质上是一个文件描述符,是用来描述文件的,在JAVA中一切都是文件。
我们在网络编程中要使用到网卡,网卡是一个硬件设备,计算机中通过操作系统来管理网卡,因此网卡就被描述为一个文件,这个文件就是Socket文件。所以说要想网络编程,就需要操作网卡,要想操作网卡,就需要先创建socket文件出来,通过读写这个socket文件来实现网络数据的传输。
网络编程中需要建立客户端和服务器模型:当我们在网上输入一个请求时,服务器接收到请求并做出相应的处理,然后将处理的结果返回给客户端,客户端这边收到服务器的响应后将结果显示出来。
Socket的分类
流套接字(使用传输层TCP协议)
JAVA流套接字通信模型
数据报套接字(使用传输层UDP协议)
Java数据报套接字通信模型
TCP协议
TCP和UDP都属于传输层协议,关于这两个协议会在后面的博客中详细介绍,现在只讨论Socket如何利用这两个协议来实现客户端和服务器之间的传输。
TCP协议的特点
- 有连接
- 可靠传输
- 面向字节流
- 有发送缓冲区和接收缓冲区
- 全双工
- 大小不受限制
对上面的特点进行分析:有连接就好比我们生活中的打电话,发起打电话的一方只有在另一方接起电话以后才能进行信息交流。
可靠传输这里先不讲,这里面的东西太多了。
面向字节流可以想象成生活中的喝水,我们在拿到一瓶水的时候,可以分几次喝完,也可以一次性喝完,水终究只有一瓶 。也就是说我们在传输数据的时候非常灵活,可以一次传输一个数据,也可以一次传输多个数据。
发送缓冲区和接收缓冲区是对这种数据流专门设置的一个相当于蓄水池的内存空间。
全双公是指双向通信,同一时间你我可以同时说话,在文件中就是既能读也能写。
大小不受限制这块主要是为了区别UDP协议。
TCP流套接字编程
服务端
主要使用的是ServerSocket API来进行编程,下面是ServerSocket的构造方法
ServerSocket(int port) 功能:创建一个服务端流套接字Socket,并绑定到指定端口
ServerSocket的主要方法
Socket accept() 开始监听指定端口(创建对象时绑定的窗口),有客户连接后,返回一个Socket对象,并基于该Socket建立与客户端的连接,否者阻塞等待。
close() 关闭此套接字
客户端
主要使用的是Socket API来进行编程,同时服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
Socket构造方法
Socket(String host,int port) 功能:创建一个客户端流套接字,并与对应IP的主机上,对应端口的进程建立连接
Socket的主要方法
getInetAddress() 功能:返回流套接字所连接的地址
getInputStream() 功能:返回此流套接字的输入流
getOutputStream() 功能:返回此套接字的输出流
下面根据一个需求演示一下它们的使用:
写一个环回网络服务器,所谓环回网络服务器是指客服端发送什么消息,服务器就返回什么消息。
下面根据网络编程模型来写代码,关于网络编程模型可以看我的上一篇博客网络编程的原理和基础概念_咸鱼吐泡泡的博客-优快云博客
//下面是客户端的代码
/**
* 客户端代码,客户端需要做的事情:
* 1.客户端构造请求并发送给服务器(在这个过程中要知道服务器的IP和端口号,IP因为是一台主机上面测试,所以就是127.0.0.1)
* 2.客户端接收服务器返回来的响应并显示
*/
@SuppressWarnings("all")
public class TcpEchoClient {
private String serverIp;//服务器IP
private int serverPort;//服务器端口号
private Socket socket;//套接字
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.socket = new Socket(serverIp,serverPort);//创建对象的同时就建立连接
}
public void start(){//启动方法
//创建一个容器,用来接收用户的输入
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();//socket.getInputStream()返回一个输入流,该方法的作用是将套接字中的字节放入输入流中
OutputStream outputStream = socket.getOutputStream()) {//socket.getOutputStream()返回一个输出流,该方法的作用是将字节写入套接字中
while(true){
//1.从键盘上读取用户输入的数据
System.out.print("->");
String request = scanner.next();
if (request.equals("exit")){
System.out.println("goodbey");
break;
}
//2.将用户输入的数据构造成请求,并发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3.从服务器上面读取响应并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.把结果显示在屏幕上
String log = String.format("req: %s; res: %s",request,response);
System.out.println(log);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 1234);
tcpEchoClient.start();
}
}
//下面是服务器程序
public class TcpEchoService {
private ServerSocket listenSocket = null;
public TcpEchoService(int port) throws IOException {
this.listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
/**
* UDP的服务器进入主循环,就直接尝试receive读取请求了,但是TCP是有连接的,现需要做的是,建立好连接,当服务器运行的时候
* 当前是否有客户端来建立连接,不确定。如果客户端没有建立连接,accept就会阻止等待,如果有客户端建立连接,此时accept就会返回一个Socket
* 对象,进一步的服务器和客户端之间的交互,就交给clientSocket来完成了
*/
Socket clientSocket = listenSocket.accept();
Connection(clientSocket);
}
}
public void Connection(Socket clientSocket) throws IOException {
String log = String.format("[%s: %d] 客户端已上线",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
while(true){
/**
* 1.读取请求并解析,可以直接通过inputStream的read把数据读到一个byte[],然后再转成一个String
* 但是比较麻烦,还可以借助Scanner来完成这个任务
*/
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
log = String.format("[%s:%d] 客户端已下线",clientSocket.getInetAddress().toString(),clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
//打印日志
log = String.format("[%s:%d] req: %s; res: %s",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
System.out.println(log);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
clientSocket.close();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoService tcpEchoService = new TcpEchoService(1234);
tcpEchoService.start();
}
}
结果:
UDP协议
User Datagram Protocol(用户数据报协议),是传输层协议
UDP协议的特点
- 无连接
- 不可靠传输
- 全双工
- 有接收缓冲区,没有发送缓冲区
- 大小受到限制,一次最多传输64K
解释上面每个特点的含义:
无连接相当于发微信,不管对方在不在线都可以发送,对方什么时候看到消息就可以什么时候回复。不可靠传输是相对于发送方来说的,因为他不知道对方是否在线。
面向数据报指的是:把一个一个的数据报作为基本单位,发送的时候,一次至少发一个数据报,接收的时候,一次至少接收一个数据报。
使用UDP协议来传输数据报,接收方要构造一个空的字节数组来存储接收到的数据。
因为UDP协议首部中有一个16位的最大长度,所以一个UDP所能传输的数据报最大长度是64K。
UDP数据报套接字编程
DatagramSocket
DatagramSocket是UDP Socket,用于发送和接收UDP数据报,DatagramSocket的构造方法如下:
DatagramSocket() 功能:创建一个UDP数据报套接字Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 功能:创建一个UDP数据报套接字Socket,绑定到本机上指定的一个端口(一般用于服务器)
DatagramSocket的方法:
receive(DatagramPacket) 功能:以DatagramPacket为单位接收数据报(如果没有接受到数据报,该方法会阻塞等待)
send(DatagramPacket) 功能:以DatagramPacket为单位发送数据报(不会阻塞等待,直接发送)
close() 功能:关闭此数据报套接字
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地址
getPort() 功能:获取到数据报中的中端口号
getData() 功能:获取数据报中的数据
同样写一个环回网络服务器:
下面是客户端的代码:
@SuppressWarnings("all")
public class UdpSocket_Client {
private String ServerIp;
private int ServerPort;
private DatagramSocket socket = null;
public UdpSocket_Client(String serverIp, int serverPort) throws SocketException {
this.ServerIp = serverIp;
this.ServerPort = serverPort;
this.socket = new DatagramSocket();
}
public void start() throws IOException {
//让用户输入一个数据
Scanner scanner = new Scanner(System.in);
while(true){
System.out.print("-> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodbey");
return;
}
//1.构造请求————构造一个发送数据报,此数据报中包含要发送的内容,同时还要包含指定目的主机的Ip和端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(ServerIp),ServerPort);
//2.发送请求
socket.send(requestPacket);
//3.读取服务器返回来的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096] ,4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//显示结果
String log = String.format("req: %s; resp: %s",request,response);
System.out.println(log);
}
}
public static void main(String[] args) throws IOException {
UdpSocket_Client udpSocket_client = new UdpSocket_Client("127.0.0.1", 333);
udpSocket_client.start();
}
}
下面是服务器的代码:
public class UdpSocket_Server {
private DatagramSocket socket = null;
/**
* port表示端口号,服务器启动的时候需要关联一个端口号。收到数据的时候,就会
* 根据这个端口号来决定把数据交给那个进程。虽然此处port写的类型是int,但是实际上端口号是一个两个字节的无符号整数
* 范围在0-65535
* @param port
* @throws SocketException
*/
public UdpSocket_Server(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
/**
* 读取请求,当前服务器不知道客户端什么时候发来请求,receive方法也会阻塞,如果真的有请求过来,此时
* receive就会返回,参数DatagramPacket是一个输出型参数,socket中独到的数据会设置到这个参数的对象中,
* DatagramPacket在构造的时候,需要指定一个缓冲区
*/
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//接收到请求
socket.receive(requestPacket);
//1.读取请求
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算出响应
String response = process(request);
/**
* 3.把响应写回到客户端,这时候也要构造一个DatagramPacket,此处给DatagramPacket中设置长度,必须是字节的个数
* 如果直接response.length()此处得到的是字符串的长度,也就是字符的个数
*/
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), requestPacket.getLength(),
requestPacket.getSocketAddress());
socket.send(responsePacket);
//打印日志
String log = String.format("[%s:%s] req:%s; resp:%s",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
request,
response);
System.out.println(log);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpSocket_Server udpSocket_server = new UdpSocket_Server(333);
udpSocket_server.start();
}
}
TCP和UDP的适用场景
- TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景;
- UDP适用于高速传输和实时性要求较高的通信领域;
- 如果传输单个数据报大于64K使用TCP,如果需要广播,使用UDP;