目录
网络编程基础概念
发送端与接受端
请求与响应
客户端与服务器
常见的客户端服务器模型
Socket套接字
回显(echo)程序
UDP版的回显程序
服务器代码
客户端代码
结果
TCP版的回显程序
服务器代码
客户端代码
结果
网络编程基础概念
发送端与接受端
发送端:发送数据的进程。发送端主机:该进程所在的主机(源主机)。
接收端:接受数据的进程。接受端主机:该进程所在的主机(目的主机)。
发送端和接收端只是相对的。
请求与响应
请求:发送端到接受端所发出的要求。
响应:接收端到发送端所回复的动作。
一般来说,一次完整的网络通信既要有请求也要有响应。
客户端与服务器
客户端:发送请求并获取服务的一端。
服务器:接受请求并提供服务的一段。
常见的客户端服务器模型
如下图
Socket套接字
Socket套接字是操作系统提供用于网络编程的技术,是基于TCP/IP协议的网络通信的基本单元。
简单的理解为,在下图中的分层里,是传输层协议和应用层协议之间的API。
传输层中最核心的两个协议就是TCP和UDP。因此Socket也提供了两种风格的API。还有一种unix,现在基本不使用了。
在Java中,JVM把这个Socket又封装了一下,以供我们使用,名字还是Socket。
回显(echo)程序
回显服务器:客户端发过来什么,原封不动的在发回去。
UDP版的回显程序
UDP的特点:无连接,不可靠传输,面向数据报,全双工。
连接:客户端和服务器之间进行通信之前先要相互联系上。
可靠传输:下篇文章详细讲UDP协议是会提到。
面向数据报:所传输的数据都是以“数据报”为单位的。
全双工:一个通信通道,同一时间内,既可以发送也可以接收数据。(因为通信通道里有8条网线)
半双工:一个通信通道,同一时间内,只能发送或接收数据。
在写程序之前,先画出一个图有更全面的认识。
服务器代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// UDP版本的回响服务器
class UdpEchoServe {
// 通过DatagramSocket实例化的对象serverSocket来操作网卡
// 操作系统把这个对象抽象成文件来管理的
private DatagramSocket serverSocket = null;
// 传输信息的单位不是简单的字节,而是包装成了DatagramPacket这个类来使用
// 这里第一个参数是一个返回型参数,客户端传过来的信息可以存到这里面
// 第二个参数是最多可以放多少空间来存信息
DatagramPacket serverPacket = new DatagramPacket(new byte[8192], 8000);
// 构造方法
// 建议服务器手动绑定端口号(1024~65535之间任意选择一个整数)
// 如果系统自动分配的话,客户端就不知道服务器是哪个端口了,就无法通信了
// 服务器的端口我们还是可以控制查询的,主动权在我们手中,客户端则是未知的
// 端口号可以定位进程本质上就是serverSocket对象绑定了端口号
public UdpEchoServe(int port) throws SocketException {
this.serverSocket = new DatagramSocket(port);
}
// 通过这个方法让服务器启动
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 如果没有收到请求,就会阻塞等待
// 请求填充到reservePacket中的数组里了
serverSocket.receive(serverPacket);
// 使用work方法代表处理请求
// 先把请求变成字符串,后续更加方便操作它
String workBeforeString = new String(serverPacket.getData(), 0, serverPacket.getLength());
String workAfterString = work(workBeforeString);
// 请求已经处理完毕,需要返回给客户端
// 把处理结果打包成DatagramPacket类型来传输 这里不能直接用字符串的长度,而是要转成字节后的长度
DatagramPacket retPacket = new DatagramPacket(workAfterString.getBytes(), workAfterString.getBytes().length,
// 同时还要从serverPacket中拿到客户端的地址(端口号和IP)
serverPacket.getSocketAddress());
// 打包完成,返回数据
serverSocket.send(retPacket);
// 在服务器这里打印一下请求响应的中间结果
System.out.printf("[请求方的IP:%s | 请求方的端口号: %d] = 请求:%s | 处理结果:%s\n",
serverPacket.getAddress().toString(), serverPacket.getPort(), workBeforeString, workAfterString);
}
}
protected String work(String str) {
// 由于是回显服务器,处理的结果就直接返回原字符串即可
return str;
}
}
public class UdpServer {
public static void main(String[] args) throws IOException {
// 指定端口号
UdpEchoServe udpEchoServe = new UdpEchoServe(9999);
udpEchoServe.start();
}
}
客户端代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
// UDP版本的回响客户端
class UdpEchoClient {
// 和客户端的一样,也是要通过这个类实例出来的对象来操作网卡
private DatagramSocket clientSocket = null;
// 同时还需要设定服务器的IP和端口,这样才能把消息发给客户端
// 服务器则不用设定,因为服务器作为被动的接受数据的一方,这数据中就包含了客户端的IP和端口
private String serverIp = null;
private int serverPort = -1;
// 构造方法
// 在构造方法中设定客户端端口、服务器IP和服务器端口
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
// 建议随机分配客户端的端口
// 因为随机分配的端口是空闲的端口,如果自己指定,则不一定是
this.clientSocket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
// 通过start方法启动客户端
public void start() throws IOException {
System.out.println("客户端启动!");
while (true) {
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
// 输入数据
String data = scanner.next();
if (data.equals("exit")) {
clientSocket.close();
break;
}
// 收到数据后打包数据 同样是以DatagramPacket为单位
// 这里把字符串转成字节,还有字节的实际个数也要传过去
// InetAddress这个类中的getByName方法可以把"127.0.0.1"这个字符串转成32位二进制数
DatagramPacket clientPacket = new DatagramPacket(data.getBytes(), data.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
// 发送数据
clientSocket.send(clientPacket);
// ... ... 等待服务器处理,然后接收处理后的数据
// 接收数据
DatagramPacket backedData = new DatagramPacket(new byte[8192], 8000);
// 把收到的数据填到数组当中
clientSocket.receive(backedData);
// 解析数据 然后使用数据
work(backedData);
}
}
// 处理返回数据的方法
private void work(DatagramPacket backedData) {
String useData = new String(backedData.getData(), 0, backedData.getLength());
// 解析好之后使用数据
System.out.println(useData);
}
}
public class UdpClient {
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9999);
client.start();
}
}
结果
如果像多创造一些客户端,在IDEA中可以勾选允许创建多个实例。
TCP版的回显程序
TCP的特点:有链接,可靠传输,面向字节流,全双工。
字节流:和之前的文件读写一样(通过字节流),所传输的数据都是转换成了字节流。
工作流程如下图
服务器代码
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// TCP版本的回显服务器
class TcpEchoServer {
// 同UDP,这里的类变成了ServerSocket来管理TCP版的服务器
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
// 手动绑定服务器端口号
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
// 启动服务器
System.out.println("服务器启动!");
// 要先和客户端建立连接
// 通过Socket这个类与客户端建立连接
// 如果客户端没有连接,accept就会进行阻塞
// Ⅰ 这里只能处理一个客户端
// Socket connectionSocket = serverSocket.accept();
// 具体的连接和收发数据给connectionWork这个方法
// connectionWork(connectionSocket);
// Ⅱ 通过多线程可以处理多个客户端——多搞几个连接
// while (true) {
// Thread threadConnection = new Thread(() -> {
// try {
// Socket connectionSocket = serverSocket.accept();
// connectionWork(connectionSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// threadConnection.start();
// }
// Ⅲ 使用线程池
while (true) {
ExecutorService threadPoolExecutor = Executors.newCachedThreadPool();
threadPoolExecutor.submit(()-> {
try {
Socket connectionSocket = serverSocket.accept();
connectionWork(connectionSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
// 这里处理一个客户端连接和收发数据
private void connectionWork(Socket connectionSocket) throws IOException {
// 能到这里说明客户端已经建立连接了·
System.out.printf("[IP: %s | 端口号: %d] 客户端上线!\n",
connectionSocket.getInetAddress().toString(), connectionSocket.getPort());
try (InputStream inputStream = connectionSocket.getInputStream();
OutputStream outputStream = connectionSocket.getOutputStream()) {
// 可能会有很多次的请求,通过循环来一直保持连接(长连接)
while (true) {
// 把读进来的请求用scanner装起来,更方便操作
Scanner scanner = new Scanner(inputStream);
// 如果请求没了,说明读完了就退出
if (!scanner.hasNext()) {
System.out.printf("[IP: %s | 端口号: %d] 客户端下线!\n",
connectionSocket.getInetAddress().toString(), connectionSocket.getPort());
break;
}
// 把请求从scanner中读出来, 不包括空格 会车符等空白字符串
String workBeforeString = scanner.next();
// 放到 work方法中加工
String workAfterString = work(workBeforeString);
// 通过OutPutStream返回结果结果
// 由于OutPutStream中没有可以直接把字符串写进去的方法
// 这里通过PrintWriter来代替写
PrintWriter printWriter = new PrintWriter(outputStream);
// 把处理后的字符串+回车符写进去
printWriter.println(workAfterString);
// 在把数据冲刷一下, 确认写进去了
printWriter.flush();
System.out.printf("[请求方的IP:%s | 请求方的端口号: %d] = 请求:%s | 处理结果:%s\n",
connectionSocket.getInetAddress().toString(), connectionSocket.getPort(), workBeforeString, workAfterString);
}
}
finally {
// 每一个客户端请求完成后就要关闭请求连接的这个资源, 否则会越来越多
// 在finally中确保一定会关闭的
connectionSocket.close();
}
}
private String work(String workBeforeString) {
return workBeforeString;
}
}
public class TcpServer {
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(5050);
tcpEchoServer.start();
}
}
客户端代码
// TCP版本的回显客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
class TcpEchoClient {
// 通过Socket这个类来连接服务器
private Socket connectionSocket = null;
public TcpEchoClient (String serverIP, int serverPort) throws IOException {
// 这里实例化的时候就与服务器建立连接了
// 同时Socket的构造方法可以识别"127.0.0.1"这样字符串类型的点分十进制
connectionSocket = new Socket(serverIP, serverPort);
}
public void start() throws IOException {
System.out.println("客户端启动! ");
try(InputStream inputStream = connectionSocket.getInputStream();
OutputStream outputStream = connectionSocket.getOutputStream()) {
while (true) {
// 先输入数据
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
String data = scanner.next();
if (data.equals("exit")) {
break;
}
// 打包数据发给服务器
// 通过PrintWriter类来包装数据
PrintWriter printWriter = new PrintWriter(outputStream);
// 这里还把 \n 写进去了,为了分割数据
printWriter.println(data);
// 确保数据发送出去了
printWriter.flush();
// ... ...等到客户端处理完成后接收结果
// 读取客户端的响应
Scanner result = new Scanner(inputStream);
String resultString = result.next();
// 最后打印结果
System.out.println(resultString);
}
}
}
}
public class TcpClient {
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 5050);
tcpEchoClient.start();
}
}
结果
有什么错误评论区指出。希望可以帮到你。