网络编程就是通过代码,来控制两个主机的进程之间的数据交互。
操作系统把网络编程的一些操作封装起来,对外提供一组 API 供程序员使用,在这里我们使用的是Socket API,它其实就是传输层提供给应用层的服务。
UDP和TCP协议都是传输层的协议。
在操作系统中,一切皆文件,网卡作为一个硬件设备,操作系统也是用文件的形式来管理网卡,此处用来管理网卡的文件就是socket。
socket就是一个文件描述符表。
当某个进程被创建出来的时候,进程就会对应的创建一个PCB,PCB中就包含一个文件描述符表,文辞打开文件,就会为对应的文件分配一个表项。
其中第三步是程序最核心的部分,其他部分都是大同小异。
用来描述一个socket对象,里面的方法有:
- receive:用来接收数据,如果数据没有过来,就会阻塞等待,如果有数据了,就会返回一个DatagramPacket对象。
- send:用来发送数据,以DatagramPacket为单位进行发送
注意:发送的时候,得知道发送得目标在哪里,接收得时候,也得知道数据从哪里来。
用来描述一个数据报,发送、接收都是以DatagramPacket为单位进行的。
服务器端:
package 回显服务UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 3020104637
* Date: 2022-08-01
* Time: 23:49
*/
public class UdpEchServer {
private DatagramSocket socket = null;
//port 表示端口号,服务器启动的时候,需要关联(绑定)一个端口号
//收到数据的时候,就会根据这个端口号来决定把数据就给哪个进程
//虽然这里的 port 写的是int类型,但是实际上是一个两个字节的无符号整数,范围为:0 ~ 65535
public UdpEchServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
//服务器一般都是维持运行 7*24h
while(true){
//1.读取请求,当前服务器不知道客户端啥时候发送过来请求,此时一直处于阻塞状态
// 如果真的有请求过来了,此时 receive 就会返回
// 参数DattagramPacket是一个输出型参数,socket读到的数据就会设置到这个对象中
// DatagramPacket 在构造的时候,需要指定一个内存缓冲区(就是一段内存空间,通常使用byte[])
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);//把receive的返回值给requestPacket
//把requestPacket对象里面的内容取出来,作为一个字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求来响应计算
String response = process(request);
//3.把响应写回到客户端,这时候也需要构造一个DatapramPacket
// 此处给DatagramPacket中设置长度,必须是“字节数的个数”
// 如果直接取response.length(),此处得到的是,字符串的长度,也就是“字符的个数”
// 当前的responsePacket在构造的时候,还需要指定这个包要发给谁
// 其实发送给的目标,就是发请求的一方,用requestPacket.getSocketAddress()来获取
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.加上日志打印,用格式化字符串的方式
String log = String.format("[%s:%d] req: %s; resp: %s",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
request,response);
System.out.println(log);
}
}
//次数的 process 方法负责的功能就是根据请求来计算响应
//由于当前是一个回显服务,于是就将客户端发的请求直接返回即可
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchServer server = new UdpEchServer(9090);
server.start();
}
}
客户端:
package 回显服务UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 3020104637
* Date: 2022-08-01
* Time: 23:49
*/
//客户端发什么,服务器回复什么
public class UdpEchClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpEchClient(String serverIp,int serverPort) throws SocketException {
this.serverIp=serverIp;
this.serverPort=serverPort;
//构造的是客户端的socket,所以得有自己的端口,此时不指定端口就会随机获取一个空闲得端口
this.socket=new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true){
//1.从标准输入读入一个数据
System.out.println("-> ");
String request=scanner.nextLine();
if(request.equals("exit")){
System.out.println("exit");
return;
}
//2.把字符串构造成一个 UDP 请求
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());
//显示这个结果
String log = String.format("req: %s; resp: %s",request,response);
System.out.println(log);
}
}
public static void main(String[] args) throws IOException {
//127.0.0.1 是一个环回ip,表示主机本身
UdpEchClient client = new UdpEchClient("127.0.0.1",9090);
client.start();
}
}
DatagramPacket的三种构造方法:
第一种构造方法的特殊之处
ServerSocket是创建TCP服务器的Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
ServerSocket 一般仅用于设置端口号和监听,真正进行通信的是服务器端的Socket与客户端的Socket,在ServerSocket 进行accept之后,就将主动权转让了。
详见: ServerSocket和socket的区别
public class TcpEchClient {
private Socket socket =null;
private String serverIp;
private int serverPort;
public TcpEchClient(String serverIp,int serverPort) throws IOException {
this.serverIp = serverIp;
this.serverPort = serverPort;
//让socket创建的同时,就 尝试和服务器建立连接,此处就发生TCP的三次握手
this.socket = new Socket(serverIp,serverPort);
}
public void start(){
Scanner scanner =new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true){
//1.从键盘上输入内容
System.out.println("->");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("exit");
break;
}
//2.把读取的内容,构造成请求
PrintWriter writer = new PrintWriter(outputStream);
writer.println(request);
writer.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 {
TcpEchClient client = new TcpEchClient("127.0.0.1",9000);
client.start();
}
}
几个注意点:
1.
2.
public class TcpEchServer {
private ServerSocket listenSocket=null;
public TcpEchServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void satrt() throws IOException {
System.out.println("服务器启动");
//可能会有多次连接
while(true){
//1.建立连接
// 当没有服务器请求建立连接的话,accept就进行阻塞
// 当没服务器请求建立连接的话,accept就会返回一个Socket对象
Socket clientSocket=listenSocket.accept();
//2.处理连接
processConnection(clientSocket);
}
}
private void processConnection(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.读取请求并解析
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不是跟随整个生命周期,而是与连接有关
//因此每个连接结束,都要进行关闭
//否则,随着socket的增多,就会出现资源泄漏的问题
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchServer server = new TcpEchServer(9000);
server.satrt();
}
}
上述版本的话,当有多个客户端发起请求时,就无法处理,原因分析:
那怎么解决呢?
可以采用多线程的方式解决这个问题。
但是上面的多线程方式,有多少个请求,就要创建多少个线程,这样下去就比较消耗资源。
现实中有的线程并不一直有活可干,这样下去就比较消耗资源,因此可以采用线程池的方式。
1.选项
2.设置过滤器
3.根据结果分析问题