目录
1、InetAdress类
2、Socket套接字
3、UDP数据报套接字编程
(1)DatagramSocket 类
(2)DatagramPacket类
(3)处理无连接问题
UdpEchoServer.java
UdpEchoClient.java
4、TCP流套接字编程
(1)工作流程
Server.java
Client.java
(2)改进
Server.java
Client.java
运行结果
(3)相关解析
socket.close()
scanner.next()的工作原理
hasNext()工作原理
Ideal同时运行多个进程设置
服务器同时处理数个客户端请求
指网络上的主机,通过不同的进程以编程的方式实现网络通信(网络数据传输)
1、InetAdress类
Java语言提供了InetAdress类,该类用于处理IP地址和主机名称(hostname)的获取
这个类并没有提供构造函数,必须利用该类的静态方法来创建对象
static InetAdress | getByName(String host) | |
static InetAdress[ ] | getAllByName(String host) | |
static InetAdress | getByAdress(byte[ ] addr) | |
static InetAdress | getByAdress(String host , byte[ ] addr) | |
static InetAdress | getLocalHost() | 返回本地主机的地址 |
static InetAdress | getLoopbackAdresst() | 返回回送地址 |
另外还有些非静态方法用于获取InetAdress对象的信息:
返回值 | 方法名 | 返回数据 |
byte[ ] | getAdress() | 原始IP地址 |
String | getHostAdress() | IP地址字符串 |
String | getHostName() | 主机名 |
String | getCanonicalHostName() | 获取此IP地址的完全限定域名 |
文章链接分享:主机与域名
2、Socket套接字
是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元
基于Socket套接字的网络程序开发就是网络编程
针对传输层协议可以将网络编程分为两类:
(1)流套接字:TCP(传输控制协议)网络通信
有连接、可靠、面向字节流、有接收/发送缓冲区的、大小不限、全双工
(适用于需要可靠数据传输的场景,如HTTP、FTP等协议的通信)
(2)数据报套接字:UDP(用户数据报协议)网络通信
无连接、不可靠、面向数据报、全双工
(适用于不需要可靠传输,但对实时性有要求的场景,如视频会议、在线游戏等)
有无连接:
通信双方不需要建立连接,可以直接发送数据报
而这里使用Socket进行通信时,双方必须先建立一个连接才能进行数据的发送和接收
面向字节流:即传输数据基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次发送
面向数据报:即传输数据是一块一块的,发送一块数据加入100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节
3、UDP数据报套接字编程
这里要用到2个类:DatagramSocket类、DatagramPacket类
(1)DatagramSocket 类
此类表示用于发送和接收数据报数据包的套接字
相关方法:
void receive(DatagramPacket p) | 从此套接字接收数据报(阻塞直到真正接收到数据报) |
void send(DatagramPacket p) | 从此套接字发送数据报 |
void close() |
(2)DatagramPacket类
上述DatagramSocket 类中我们发现往这个Socket通道中发送接收都是以一个UDP数据报为单位进行的
但是这个数据报DatagramPacket内部是不能自行分配内存空间,需要手动创建空间,需要通过构造方法往里面放一个字节数组
(3)处理无连接问题
我们知道UDP是无连接的,意味着服务器与客户端双方并不存储对方的地址与端口号,但是若双方都不知道对方在哪儿,这就传输通信不了了呀!
银行(服务端)是不需要知道取钱的人(客户端)的地址的,但客户端要知道银行的地址才能找上门请求服务的,所以客户端这边是需要记下指定服务端的地址的。
但若你还请求了办理证件这种服务,是需要后期邮寄上门的,那么你在请求服务时就会把地址告诉银行。
按照这样的逻辑我们来看下在UDP这里是如何处理无连接问题的
既然UDP自身无法记住,那就由我们另外记录下来
客户端把服务端的地址、端口设置为成员变量,在构造方法中进行赋值,这样就记录了服务端的地址与端口,那么在下面一些列请求中就可以使用了(相当于记下了)
public UdpEchoClient(String ip,int port) throws SocketException {
socket =new DatagramSocket();
serverIp=ip;
serverPort=port;
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
client.start();
}
而服务端只需要给自己指定端口号即可(给自己定个固定地址,让客户端找上门服务)
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
我们可以发现服务端的端口号是指定的固定的,但是客户端的Socket方法却是让系统自动随机分配一个空闲端口。
原因很简单,试想一下,若同时有数个相同地址相同端口的客户端向同一个服务端发起请求,相同个端口无法区分,那么服务端处理谁呢?所以由系统自动随机分配一个空闲端口可以避免端口冲突!
UdpEchoServer.java
下面我们来写一个简单的回显服务器:(收到什么就返回什么)
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//1、创建数据报
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
//2、接收并解析数据报
socket.receive(requestPacket);
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//3、计算响应并返回给客户端
String response=process(request);
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().toString(),requestPacket.getPort(),request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
UdpEchoClient.java
public class UdpEchoClient {
private DatagramSocket socket=null;
private String serverIp="";
private int serverPort=0;
public UdpEchoClient(String ip,int port) throws SocketException {
socket =new DatagramSocket();
serverIp=ip;
serverPort=port;
}
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、发送请求数据报
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),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();
}
}
上述只是一个简单的回显服务,若你还想要一个处理复杂点的服务的服务端可以直接继承回显服务端再重写相关方法。比如下面是一个翻译服务:
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//此处可以往这个表里插入几千几万个这样的英文单词
dict.put("dog","小狗");
dict.put("cat","小猫");
dict.put("pig","小猪");
}
//重写process方法,在重写的方法中完成翻译的过程(翻译的本质就是”查表“)
@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();
}
}
4、TCP流套接字编程
用到两个关键的类:
- SeverSocket类:用于创建服务器绑定端口
- Socket类:该类实现客户端套接字,套接字是两台机器之间通讯的端点
(1)工作流程
UDP中是各自有一个DatagramSocket类,客户端手动存储服务端的地址与端口,通过数据报发送请求到服务端,服务端就可以接收数据报中的请求、客户端的地址端口,这样就又可以通过数据报把响应返回给客户端,客户端就获得响应,完成网络通信。
这个TCP可就牛杯了。
服务端通过ServerSocket创建服务器绑定端口,等待接收客户端在内核已建立好的连接对象。
客户端Socket s=new Socket(String serverIP,serverPort)就可以与服务端建立连接,服务端这边就能直接获得连接对象Socket对象,就可以直接对Socket对象进行一系列操作(读取请求、写入响应)。
也就是说这下服务端和客户端都是直接对同一个Socket对象进行操作!双方都用这一个Socket对象获取字节输入输出流,这样我都可以直接对同一个socket进行读写了!
Socket是操作系统中的一个概念,本质上是一种特殊的文件,Socket就属于是把“网卡”这个设备给抽象成文件了。往Socket中写数据,就相当于通过网卡发送数据;从socket文件读数据就相当于通过网卡接收数据。
按照流程来一一梳理下连接完之后双方交互的过程
- 客户端 out.write(request)写入请求;
- 服务端 in.read()读取请求,服务端 out.write()写入响应;
- 客户端 in.read()读取响应
根据这样的流程先简单模拟下:
Server.java
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(9088);
System.out.println("服务端上线");
while (true){
Socket socket = serverSocket.accept();
System.out.println("客户端连接成功");
try(InputStream in = socket.getInputStream();OutputStream out=socket.getOutputStream()) {
byte buf[]=new byte[1024];
int n = in.read(buf);
String request=new String(buf,0,n);
String response="hello";
out.write(response.getBytes());
}finally {
socket.close();
System.out.println("客户端下线");
}
}
}
}
Client.java
public class Client {
public static void main(String[] args) throws IOException {
Socket socket=new Socket("127.0.0.1",9088);
try(InputStream in = socket.getInputStream(); OutputStream out=socket.getOutputStream()){
String request="hello";
out.write(request.getBytes());
byte buf[]=new byte[1024];
int n = in.read(buf);
String response=new String(buf,0,n);
System.out.println (new String(buf,0,n));
}
}
}
客户端连续连接两次运行结果如下:
(2)改进
Server.java
public class Server {
ServerSocket server=null;
public Server(int port) throws IOException {
server=new ServerSocket(port);
System.out.println("服务端上线");
}
public void start() throws IOException {
while (true){
Socket socket=server.accept();
System.out.printf("[%s:%d] 客户端上线! \n",socket.getInetAddress(),socket.getPort());
Thread t=new Thread(()->{
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
private void processConnection(Socket socket) throws IOException {
try(InputStream in=socket.getInputStream();OutputStream out=socket.getOutputStream()){
Scanner sc=new Scanner(in);
PrintWriter pw=new PrintWriter(out);
while (true){
if (!sc.hasNext()){
//若没有内容则进入阻塞等待输入内容;若有内容则继续循环处理请求
//若客户端断开连接(下线),sc就知道不会再有输入内容,就进入if()跳出循环,请求处理完毕,此线程结束
System.out.printf("[%s:%d] 客户端下线! \n",socket.getInetAddress(),socket.getPort());
break;
}
String request=sc.next();
String response=process(request);
pw.println(response);//服务端写入时末尾加了“\n”,作为客户端读取结束标志
pw.flush();
System.out.printf("[%s,%d] req=%s resp=%s \n",socket.getInetAddress(),socket.getPort(),request,response);
}
}finally {
socket.close();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
Server server=new Server(9789);
server.start();
}
}
Client.java
public class Client {
Socket socket=null;
public Client(String serverIp,int serverPort) throws IOException {
socket=new Socket(serverIp,serverPort);
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
try(InputStream in = socket.getInputStream(); OutputStream out=socket.getOutputStream()){
Scanner sc=new Scanner(in);
PrintWriter pw=new PrintWriter(out);
while (true){
System.out.println("请输入请求->");
String request= scanner.next();
pw.println(request);//客户端写入时末尾加了“\n”,作为服务端读取结束标志
pw.flush();
String response=sc.next();
System.out.printf("返回响应:");
System.out.println(response);
}
}
}
public static void main(String[] args) throws IOException {
Client client=new Client("127.0.0.1",9789);
client.start();
}
}
运行结果
(3)相关解析
socket.close()
前面写过的DatagramSocket、ServerSocket都没有close(),因为这两个在整个程序中都只有一个对象,贯穿整个程序,一旦程序结束这两个对象都会自动被销毁,不存在资源泄漏的问题
但是这里服务端接收到的Socket对象则是在循环中每有一个新的客户来建立连接都会创建一个的
并且这个对象最多时用到该客户退出(断开连接)。此时若有很多个客户都来建立连接就意味着会创建很多个Socket对象,当连接断开此时这个对象就会占据着文件描述表的位置。
而在上面的
try(InputStream in = socket.getInputStream();OutputStream out=socket.getOutputStream())
只是关闭了Socket对象上自带的流对象,而并没有关闭Socket对象本身
scanner.next()的工作原理
用于读取输入,直到遇到空格、Tab键或Enter键等分隔符为止,返回的是内容是连续的有效字符序列,而不包含任何前导或尾随的空白字符
scanner.next()方法提供了一个简单的方式来读取和处理以空白字符分隔的字符串输入
比如输入"abc xyz",则会读取并返回"abc",接着若再次调用next()它将返回"xyz"
在这个过程中,输入的空格被next()视为分隔符,从而确保了每次调用next()时都会返回一段完整的数据
hasNext()工作原理
用来判断缓冲区内是否还有内容,若还有内容,则返回true;若没有内容,不会返回false,而是堵塞当前程序,并且等待输入内容
Ideal同时运行多个进程设置
再点击右上角三角形运行图标就可以多个进程运行同一个代码了
服务器同时处理数个客户端请求
上面的代码我们给出的解决方案是每来一个客户端就新创建一个线程处理请求,这样的解决办法在数个客户端请求的情况下,会频繁地来进行建立、断开连接,会导致服务器频繁地创建、销毁线程,这样的开销是巨大的!
为了进一步优化,我们可以使用线程池、协程、或者I/O多路复用等手段。
由于作者只会线程池,所以这里就写下线程池:
public void start() throws IOException {
ExecutorService service= Executors.newCachedThreadPool();
while (true){
Socket socket=server.accept();
System.out.printf("[%s:%d] 客户端上线! \n",socket.getInetAddress(),socket.getPort());
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}