目录
1. 套接字
2. 基于UDP 套接字实现的简单客户端 服务器程序
3. 基于TCP套接字实现的简单客户端 服务器程序
1. 套接字
之前我们有分享到协议分层这个概念,其中就讲到上层协议调用下层协议,下层协议给上层协议提供支持,这里支持指的是就是socket套接字,它是操作系统给应用程序提供的一组用于网络编程的API(传输层给应用层的支持).
2. 基于UDP 套接字实现的简单客户端 服务器程序
UDP是传输层协议之一,它的特点是无连接,不可靠,面向数据报传输,半双工(数据传输允许数据在两个方向上传输,但在某一时刻,只允许数据在一个方向传输).
● UDP的套接字
DatagramSocket(指定端口号为服务器,不指定端口号则为客户端)类是用于发送和接收数据报的套接字,而DatagramPacket类是表示UDP数据报,客户端和服务器双方传输的都是数据报.
1) DatagramSocket常用构造方法有:
//不指定端口号,表示客户端
DatagramSocket socket = new DatagramSocket();
//指定端口号,表服务端
DatagramSocket socket = new DatagramSocket(port);
其它的构造方法参考jdk文档:
主要的普通方法有receive():从此套接字接收数据报包 :
它的参数是一个输出型参数,传入一个空的数据报,那么就会将从网卡读取到的数据填充进去.打个比方,就是你拿一个空的饭盒去食堂窗口打菜,有菜食堂就会往你饭盒打菜.等你从窗口处拿到你的饭盒,你的饭盒就已经有菜了.这时你的饭盒就相当于输出型参数.当然如果窗口菜还没炒好,你就需要等待.所以receive()方法可能会产生阻塞.
send():从此套接字发送数据报包:
,close():关闭该数据报套接字.
注:要确定该数据报套接字不会再使用才能关闭该资源.还有其它很多该套接字的方法,大家想了解的话可以去查看jdk文档学习.
2) DatagramPacket常用构造方法有:
其它的构造方法参考jdk文档:
其它的方法主要分为获取和设置:包括(数据报发送或接收的计算机IP地址,端口号),数据缓冲区,发送或接收数据的长度等等,根据我们自己的实际需求去调用.这里我们用到的就是以下方法:
● 示例代码
这里先将示例代码展示,然后再根据代码跟大家分享一下我的思路.
//服务器
public class UdpEchoServer {
DatagramSocket socket;
// 1.初始化一个带端口号的DatagramSocket对象(作为服务器)
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 2.准备一个数据报(空的饭盒),调用receive方法阻塞等待接收客户端的请求数据
DatagramPacket request = new DatagramPacket(new byte[2023],2023);
// 5.收到客户端发送的数据报数据并填入步骤2准备的数据报中
socket.receive(request);
// 将请求数据转换为字符串
String req = new String(request.getData(),0,request.getLength());
// 6.根据请求计算响应(这里由于是回显服务器,所以请求和响应是一样的)
String response = process(req);
// 7.将响应数据放入一个数据报中,使用send()方法返回给客户端
DatagramPacket resp = new DatagramPacket(response.getBytes(),0,response.getBytes().length,request.getSocketAddress());
socket.send(resp);
// (最后打印请求和响应数据)
System.out.printf("[ip:%s port:%s] req:%s res:%s\n",resp.getAddress(),socket.getPort(),req ,response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
//客户端
public class UdpEchoClient {
DatagramSocket socket;
// 发送请求数据报的服务端ip地址和端口
public String ip;
public int port;
// 1.初始化一个不带端口号的DatagramSocket对象(作为客户端)
public UdpEchoClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
this.ip = ip;
this.port = port;
}
public void start() throws IOException {
System.out.println("客户端启动");
while (true) {
Scanner s = new Scanner(System.in);
String temp = s.nextLine();
// 3.将请求的数据放入一个数据报中,使用send()发送
DatagramPacket request = new DatagramPacket(temp.getBytes(),temp.getBytes().length,InetAddress.getByName(ip),port);
socket.send(request);
// 4.准备一个空的数据报接收响应数据,调用receive()方法阻塞等待服务器返回的响应数据
DatagramPacket response = new DatagramPacket(new byte[2023],2023);
// 8.收到响应数据报,将响应数据放入步骤4准备的空数据报中
socket.receive(response);
// 将数据报数据转换为字符串
String res = new String(response.getData(),0,response.getLength());
// (最后打印请求和响应数据)
System.out.printf("req:%s res:%s\n", temp,res);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
● 根据示例代码分析服务器和客户端的交互流程(图解)
注: ① 应用层可以通过编程实现UDP全双工通信,在某一时刻既可以发送数据,又可以接收数据.但在网络通信中,UDP本身仍然是半双工的协议.
② 这里的socket其实是一个特殊的文件,是网卡这个硬件设备的抽象表示(我们可以通过它间接读取或修改网卡数据).
3. 基于TCP套接字实现的简单客户端 服务器程序
TCP也是传输层协议之一,它的特点是有连接,可靠传输,面向字节流,全双工(允许数据在两个方向同时传输).
● TCP的套接字
可以分为两类,一个是ServerSocket类,它是服务器套接字,给服务器使用(一定要绑定端口号),另一个是Socket类,也是个套接字,但是客户端和服务器都可以使用它(和DatagramSocket相似,没绑定端口号给客户端使用,绑定端口号给服务器使用).
1) ServerSocket常用构造方法有:
其它构造方法:
它的常用普通方法:accept() 与客户端建立连接(得到的是一个Socket对象):
close() 关闭套接字(一定要确定该套接字不再使用之后再关闭)
2) Socket常用构造方法有:
其它构造方法:
Socket中我们在这里用到的普通方法:
其它方法大家可以根据需求查看jdk文档使用.
● 示例代码
// 服务器
public class TcpEchoServer {
ServerSocket socket;
public TcpEchoServer(int port) throws IOException {
// 该服务器位于哪个端口
socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
// 请求连接
Socket clientSocket = socket.accept();
// 可连接多个客户端,每个客户端在一个线程中处理自己的逻辑
Thread thread = new Thread(() -> {
try {
processClient(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
thread.start();
}
}
private void processClient(Socket clientSocket) throws IOException {
System.out.println("客户端上线!");
// try catch with resource操作会让这里创建的字节流在使用后自动关闭
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 使用字符流处理
Scanner input = new Scanner(inputStream);
PrintWriter print = new PrintWriter(outputStream);
while (true) {
// 读取到字符流末尾
if (!input.hasNext()) {
System.out.println("客户端下线!");
break;
}
// 获取请求
String request = input.next();
// 处理请求并返回响应
String response = process(request);
// 写回响应
print.println(response);
//刷新缓冲区
print.flush();
//打印结果
System.out.printf("[ip:%s port:%s] request:%s response:%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(4048);
server.start();
}
}
// 客户端
public class TcpEchoClient {
Socket socket;
public TcpEchoClient(String ip,int port) throws IOException {
// 连接对应ip地址及端口的服务器
socket = new Socket(ip,port);
}
public void start() throws IOException {
System.out.println("客户端开启");
Scanner s = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner input = new Scanner(inputStream);
while (true) {
// 发送请求
String request = s.next();
printWriter.println(request);
// 刷新缓冲区
printWriter.flush();
// 得到响应
String response = input.next();
// 打印结果
System.out.printf("request:%s response:%s\n",request,response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",4048);
client.start();
}
}
注:我们这里约定请求和响应都是字符串,需要使用字符流来处理.
● 根据示例代码分析服务器和客户端的交互流程(图解)
注:这里缓冲区策略和线程池策略类似,将数据先写到缓冲区,等缓冲区满了再统一自动写入网卡.我们这里手动刷新缓冲区是为了将数据直接写入网卡中.
分享完毕~欢迎大家一起学习讨论~