目录
🌟需要知道
1、什么是网络编程?
2、怎么进行网络编程?
3、TCP与UDP的区别? (面试题)
🌟一、UDP
🌈1、UDP数据报套接字编程
🌈2、实现一个简单的UDP回显服务器与客户端
🌟二、TCP
🌈1、TCP流套接字编程
🌈2、实现一个简单的TCP回显服务器与客户端
🌟需要知道
1、什么是网络编程?
网络编程,指的是网络上的主机通过不同的进程以程序的方式来实现网络通信(网络数据传输)。也可以是同一个主机的不同进程。比如MySQL的服务端和客户端,在开发环境中一般都是同一台主机上运行的两个不同的程序。
(1)客户端:服务的使用方-->请求:一般是客户端主动发起,表示目的;
(2)服务器:服务的提供方-->响应:一般是服务器根据客户端的请求计算出来的结果。
2、怎么进行网络编程?
针对网络编程,操作系统提供了用于网络编程的技术,称为Socket套接字,是系统提供的专门用于实现网络编程的一套API。
应用程序在应用层,操作系统工作在传输层,Socket套接字就是在传输层对应用层提供的API支持。(Java对每种操作系统都做了进一步的封装,JDK中提供的API是我们学习的目的)。传输层最知名的协议就是TCP和UDP。
3、TCP与UDP的区别? (面试题)
面试题1:TCP与UDP的区别?
TCP 流套接字 UDP 数据报套接字 TCP即Transmission Control Protocol传输控制协议,传输层协议。 UDP即User Datagram Protocol用户数据报协议。传输层协议 有连接 无连接 可靠传输 不可靠传输 面向字节流 面向数据报 全双工(有接收缓冲区,也有发送缓冲区) 全双工(有接收缓冲区,也有发送缓冲区) 大小不限 大小受限,一次最多传输64K 传输数据是基于IO流,没有边界多次发送,多次接收 传输数据是一个整体,不能分开发送。
举个栗子🌰
(1)有无连接:
TCP相当于打电话:接收方必须要接通电话之后双方才可以通信;
UDP相当于发短信:不需要对方开机都可以进行;
(2)可靠传输:
如果数据包在传输过程中丢了,那么TCP会有重传机制,UDP则是丢了就丢了。
(3)面向字节流:
TCP打电话的时候是说一个字对方听见一个字;
UDP发短信是发一条条信息,是一整段的,对方收到一整条信息之后才可以阅读;
(4)全双工(有发送缓冲区也有接收缓冲区):
可以打电话也可以接电话,可以发短信也可以收短信。
(5)大小不限:
打电话的时候时间不受限制;
发短信的时候 字数有限制。
🌟一、UDP
🌈1、UDP数据报套接字编程
🍅DatagramSocket API:DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
(1)DatagramSocket 构造方法
方法名 | 说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
(2)DatagramSocket 普通方法
方法名 | 说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
🍅DatagramPacket API:DatagramPacket 是UPD Socket接收和发送的数据报。存放消息的具体内容。
(1)DatagramPacket 构造方法
方法名 | 说明 |
DatagramPacket(byte[] buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数保存在字节数组(第一个参数buf中),接收指定长度(第二个参数length中) |
DatagramPacket(byte[] buf,int offset,int length,Socket Address address) | 构造一个DatagramPacket用来发送数据报,发送的数据为字节数组(第一个参数buf中),从0到指定长度(第二个参数是Length)。address指定目的主机的IP和端口号。 |
(2)DatagramPacket方法
方法名 | 说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或者从发送的数据报中,获取接收端主机IP。 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或者从发送的数据报中,获取接收端主机的端口号。 |
byte[] getData() | 获取数据报中的数据。 |
构造UDP发送的数据报时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建。
🍅InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 | 方法说明 |
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
🌈2、实现一个简单的UDP回显服务器与客户端
回显:指的就是没有特殊的业务,客户端发送什么服务器就返回什么。
(1)客户端实现:
public class UDPEchoClient {
//定义一个用于客户端的DatagramSocket
private DatagramSocket client;
//定义服务器的端口号
private String serverIP;
//定义服务器的IP地址
private int port;
// 定义SocketAddress地址
private SocketAddress address;
/**
* 构造方法:指定服务器的IP和端口号
* @param serverIP
* @param port
*/
public UDPEchoClient(String serverIP,int port) throws SocketException {
this.client = new DatagramSocket();
this.serverIP = serverIP;
this.port = port;
this.address = new InetSocketAddress(serverIP,port);
}
public void clientStart() throws IOException {
System.out.println("客户端已经启动");
//循环接收用户输入
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("->");
/**
* 获取用户输入的内容并发送给服务器;然后获取服务器返回的内容并解析
*/
String userRequest = sc.nextLine();
//1、将用户输入的请求包装成DatagramPacket
DatagramPacket userRequestPacket = new DatagramPacket(userRequest.getBytes(StandardCharsets.UTF_8),
userRequest.getBytes().length,
address);
//2、发送数据
client.send(userRequestPacket);
//3、接收响应
DatagramPacket respondPacket = new DatagramPacket(new byte[1024],1024);
//4、在receive方法中填充响应数据
client.receive(respondPacket);
//5、解析响应数据
String respond = new String(respondPacket.getData(),0,respondPacket.getLength(),"UTF-8");
//打印日志
System.out.printf("request: %s, response: %s.\n", userRequest, respond);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient client = new UDPEchoClient("127.0.0.1",9999);
//启动客户端
client.clientStart();
}
}
(2)服务端实现版本1
实现功能:客户端输入一个值,服务器接收并将该值原封不动的返回。
public class UDPEchoServer {
//定义一个用于服务器端的DatagramSocket
private DatagramSocket server;
/**
* 构造方法:完成服务器的初始化
* @param port
*/
public UDPEchoServer(int port) throws Exception {
//1、如果端口不合法,则提示错误
if(port < 1024 || port>65535){
throw new Exception("端口号必须在1024-65535之间");
}
//2、初始化服务器端的UDP服务
this.server = new DatagramSocket(port);
}
/**
* 服务器接收发来的消息
*/
public void serverStart() throws IOException {
System.out.println("服务器已经启动");
//循环接收用户请求
while (true){
/**
* 接收客户端发来的数据:创建DatagramPacket里面用来存放的接收来的消息体-> receive接收DatagramPacket ->解析数据
*/
//1、创建一个用于接收请求数据的DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
//2、接收请求,将真实的内容填充到requestPacket
server.receive(requestPacket);
//3、从requestPacket中获取数据
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
//4、根据获取到的请求发出响应
String respond = processor(request);
/**
* 发送响应:也要进行数据的封装
*/
//5、将发出的响应也封装到DatagramPacket中:requestPacket.getSocketAddress()是从请求中获取到的IP地址
DatagramPacket respondPacket = new DatagramPacket(respond.getBytes(StandardCharsets.UTF_8),respond.getBytes().length,requestPacket.getSocketAddress());
//6、发送数据
server.send(respondPacket);
//打印日志
System.out.printf("[%s:%d] request: %s,respond: %s.\n",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
request,
respond);
}
}
/**
* 服务器对发来的消息做出反应
* @param request
* @return
*/
public String processor(String request) {
return request;
}
public static void main(String[] args) throws Exception {
UDPEchoServer server = new UDPEchoServer(9999);
server.serverStart();
}
}
(2)服务端实现版本2:字典服务器
实现功能:客户端输入一个值,如果map集合中存在该值,则服务器返回该值对应的value值;如果不存在该值,服务器返回“查无此值”。
public class UDPDictServer extends UDPEchoServer {
//定义一个map集合
private Map<String,String> map = new HashMap<>();
/**
* 构造方法:完成服务器的初始化
* @param port
*/
public UDPDictServer(int port) throws Exception {
super(port);
//初始化字典内容
map.put("dog","小狗");
map.put("cat","小猫");
map.put("pig","小猪");
map.put("rabbit","兔纸");
map.put("tiger","小脑斧");
}
/**
* 重写 处理请求的方法:如果不存在key值,就返回默认值
* @param request
* @return
*/
@Override
public String processor(String request) {
return map.getOrDefault(request,"查无此词");
}
public static void main(String[] args) throws Exception {
UDPDictServer server = new UDPDictServer(9999);
server.serverStart();
}
}
问题:如果需要允许多个客户端同时运行:
结果:
🌟二、TCP
🌈1、TCP流套接字编程
🍅ServerSocket API 是创建TCP服务端Socket的API。
(1)ServerSocket 构造方法
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并制定到指定端口 |
(2)ServerSocket 方法
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Scoket建立与客户端的连接,否则阻塞等待。(网络通讯过程中对数据进行了封装,InputStream输入流从网卡缓冲区读取数据,OutputStream输出流,将数据写到网卡缓冲区,相当于发送数据。) |
void close() | 关闭此套接字 |
🍅Socket API: Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
(1)Socket 构造方法
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接(通过IP和端口可以确定网络上的主机,进程) |
(2)Socket 方法
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
🌈2、实现一个简单的TCP回显服务器与客户端
(1)服务器端代码
public class TCPEchoServer {
//声明一个用于服务端的Socket对象
private ServerSocket server;
/**
* 通过指定端口号来实例化服务器
* @param port 端口号
* @throws IOException
*/
public TCPEchoServer(int port) throws IOException {
//端口的合法性校验
if(port > 65535 || port < 1024){
throw new RuntimeException("端口号要在1024-65535之间");
}
//实例化ServerSocket并制定端口
this.server = new ServerSocket(port);
}
/**
* 服务器开始工作
* @throws IOException
*/
public void start() throws IOException {
System.out.println("服务器启动成功");
//1、循环接收客户端的输入
while (true){
//accept():如果有客户端连接就返回一个Socket对象,没有的话就阻塞等待
Socket clientSocket = server.accept();
//2、服务器对接收到的客户端数据进行处理
processConnections(clientSocket);
}
}
/**
* 服务器对接收到的客户端数据进行处理
* @param clientSocket 客户端数据
*/
private void processConnections(Socket clientSocket) throws IOException {
//打印日志:也可以使用printf方式打印
String clientInfo = MessageFormat.format("[{0}:{1}] 客户端已经上线",clientSocket.getInetAddress(),clientSocket.getPort());
System.out.println(clientInfo);
//1、处理数据之前要先获取一下输入输出流
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//2、循环处理用户的请求
Scanner requestScanner = new Scanner(inputStream);
while (true){
//3、如果没有获取到用户输入:就说明客户端已经下线,关闭流并打印日志
if(!requestScanner.hasNext()){
clientInfo = MessageFormat.format("[{0}:{1}] 客户端已经下线.",clientSocket.getInetAddress(),
clientSocket.getPort());
System.out.println(clientInfo);
break;
}
//4、获取真实的用户请求数据
String request = requestScanner.nextLine();
//4-1根据请求计算响应
String respond = process(request);
//5、将响应写回客户端(参考文件操作与IO一节:PrintWriter的使用)
PrintWriter printWriter = new PrintWriter(outputStream);
//5-1 写入输出流
printWriter.println(respond);
//5-2 强制刷新缓冲区
printWriter.flush();
//6、打印日志
clientInfo = MessageFormat.format("[{0}:{1}],request:{2},response:{3}",
clientSocket.getInetAddress(),
clientSocket.getPort(),
request,
respond);
System.out.println(clientInfo);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
/**
* 接收客户端请求并返回:回显
* @param request 客户端用户的请求
* @return
*/
private String process(String request) {
return request;
//改进为一个简单的消息回复
// System.out.println("收到新消息");
// Scanner sc = new Scanner(System.in);
// String respond = sc.nextLine();
// return respond;
}
public static void main(String[] args) throws IOException {
TCPEchoServer server = new TCPEchoServer(9999);
server.start();
}
}
(2)客户端代码
public class TCPEchoClient {
//定义一个用于客户端的Socket对象
private Socket clientSocket;
/**
* 初始化客户端Socket
* @param serverIP 服务器IP地址
* @param serverPort 服务器端口号
* @throws IOException
*/
public TCPEchoClient(String serverIP,int serverPort) throws IOException {
this.clientSocket = new Socket(serverIP,serverPort);
}
/**
* 启动客户端
*/
public void start() throws IOException {
System.out.println("客户端已经启动");
//inputStream 读取数据,outputStream写入数据
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//循环处理用户的输入
while (true){
System.out.println("->");
//1、接收用户的输入内容
Scanner sc = new Scanner(System.in);
String request = sc.nextLine();
//2、发送用户请求
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//3、强制刷新缓冲区
printWriter.flush();
//4、接收服务器的响应
Scanner respondScanner = new Scanner(inputStream);
//5、获取响应数据
String respond = respondScanner.nextLine();
//打印响应数据
System.out.println("接收到服务器的响应:"+respond);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭
clientSocket.close();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient client = new TCPEchoClient("127.0.0.1",9999);
client.start();
}
}
问题:当打开多个客户端的时候,发现多个服务器并不能同时处理多个请求。
分析原因:
解决办法1:可以手动断开让客户端1。此时客户端2发送数据就有响应了。但是前提还是只能执行一个客户端。
解决办法2:实现多个客户端并行运行——>多线程实现。
为了实现服务器可以处理多个客户端连接,那么可以为每一个客户端创建一个新的线程,那么请求的处理单独在子线程中取执行。
改动:
实验结果:
但是上述创建线程的方式也存在问题:目前来说每接收一个客户端请求就会创建一个新的线程,如果有1w个连接就要创建1w个线程,非常消耗系统资源。C10k现象(非常消耗系统资源导致系统崩溃)。
解决方式3:因此优化频繁创建线程的方式:使用线程池。
演示:
如果启动多个实例,就会抛出端口已经被占用的异常。因为系统通过某个端口可以确定一个进程。
等待是煎熬的,但又是充满希望的,因为等到结果出来的那一刻,一切都是值得的。