具体的网络编程 就是写一个应用程序 使这个程序可以使用网络通信
传输层提供协议 主要有两个
- UDP
- TCP
提供了两套不同的的api (socket api)
UDP 和 TCP 特点对比
UDP: 无连接 不可靠传输 面向数据报 全双工
TCP: 有连接 可靠传输 面向字节流 全双工
什么是有链接无连接?
有链接:
JDBC 先创建一个DataSource 再通过DataSource创建Connection Connection就是连接
例如 我们日常生活中的打电话 先拨号 知道对方接通才算是完成建立连接
所以 TCP进行编程的时候 也是存在类似建立连接的过程
无连接:
例如 发微信/短信 不需要建立连接就能进行通信 进行UDP编程时存在类似过程
连接(Connection)和链接(Link)
连接是保存对方的状态
链接是快捷方式
什么是可靠传输和不可靠传输?
可靠传输 A尽可能把消息传给B 并且在传输失败的时候 A能感知到(TCP能做到 传输效率降低)
不可靠传输则不能感知到(UDP不能做到 传输效率更高)
什么是面向字节流和面向数据报
TCB和文件操作类似 都是流式的 这里传输的单位是字节 称为字节流
UDP是面向数据包 读写的基本单位是一个UDP数据报(包含了一系列数据的属性)
什么是全双工
全双工一个通道 双向通信
半双工一个通道 只能单向通信
UDP的socket api
两个核心类
- DatagramSocket
代表一个Socket对象
操作系统 使用文件的概念 管理一些软硬资源
网卡操作系统也是文件的方式来管理的
表示网卡的这类晚间称为Socket文件
Java中的Socket对象就对应这里系统的socket文件
要进行网络通信必须得先有socket对象
DatagramSocket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
| DatagramSocket(intport) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
| DatagramSocket 方法 | 方法说明 |
|---|---|
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
数据报的构造方法DatagramPacket API
| 方法签名 | 方法说明 |
|---|---|
| DatagramPacket(byte[] buf, int length) | 造一个DatagramPacket以用来接收数据报,接收的数据存在字节数组(第一个参数buf中接收指定长度(第二个参数length) |
| DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket 作为UDP数据报通过手动指定的byte[]作为存储数据的空间
DatagramPacket 方法
| 方法签名 | 方法说明 |
|---|---|
| InetAddressgetAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
| byte[] getData() | 获取数据报中的数据 |
要进行网络通信的时候要有五元组 源ip 源端口 协议类型 目的ip 目的端口此时就需要给客户端和服务器各自分配端口号 客户端口给服务器发数据 源端口就是客户端的端口 目的端口就是服务器端口反之一样所以此时需要客户端和服务器需要一个端口号在服务器这使用那个端口是手动指定的 客户端使用那个端口是系统自动分配的对于服务器来说 需要有一个固定的端口号 方便其他客户端找到一个客户端的主机上面运行的程序很多 不知道手动指定的端口是不是被别的程序占用了 所以让服务器分配更好
基于上述api动手写udp客户端服务器
回显服务器(echo server) 客户端发啥 服务器返回啥

在编写网络程序的时候 经常见到这种异常
最经典的情况端口号被占用
一个服务器要给很多个用户服务
不知道客户端什么时候来
所以服务器只能随时准备随时客户端来了就随时提供服务
public void start(){
System.out.println("服务器启动");
while (true){
//反复的,长期的执行针对客户端请求处理的逻辑
//一个服务器 运行过程中 要做的事情 三个核心
//1.读取请求 并解析
//2.根据请求,计算出响应
//3.把响应写会给客户端
}
}

在这个方法中 参数的DatagramPacket是一个 输出型参数
传入receive的是一个空的对象 receive内部就会把这个空的对象的内容给填充上 当receive执行结束 于是就得到了一个装满内容的DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096)//不能超过64kb;
这个对象用来保存数据的内存空间是需要手动指定的
不会像集合类 内部有自己管理内存的能力
服务器程序一启动 就会进入循环 进入到receive 如果没有客户端请求就会进入阻塞状态
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP的回响服务器
//客户端发的请求是什么 服务器返回的响应就是什么
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);
socket.receive(requestPacket);
//2.根据请求,计算出响应
//3.把响应写会给客户端
}
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9527);
udpEchoServer.start();
}
}

这个代码就要构造一个DatagramPacket 把响应发回给客户端

response.getBytes().length和response.getBytes()区别
就是 String s s.length() 和s.getBytes().length的区别
字符串是纯英文此时字节数和字符数是一样的但是如果有特殊符号(中文)就会出现问题 因为一个是以字符为单位 一个是以字节为单位的
(第三个参数)把数据报发给客户端就需要知道客户端的IP和端口
DatagramPacket这个对象里就包含着通信双方的ip和端口
服务器端完整代码
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP的回响服务器
//客户端发的请求是什么 服务器返回的响应就是什么
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);
socket.receive(requestPacket);
//可以把客户端的数据进行一个字符串的转化
String reques = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求,计算出响应
String response = process(reques);
//3.把响应写会给客户端
//此时需要告知网卡要发的内容是什么 要发给谁
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//记录日志 方便观察程序执行
System.out.printf("[%s:$d] req: %s, resp: %s\n",requestPacket.getSocketAddress().toString(),requestPacket.getPort(),
reques, response );
}
}
public String process(String reques){
return reques;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9527);
udpEchoServer.start();
}
}
客户端完整代码
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//客户端程序
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
//服务器的ip和服务器的的端口
public UdpEchoClient(String ip, int prot) throws SocketException {
serverIp = ip;
serverPort = prot;
//这个让系统自动分配空闲端口
socket = new DatagramSocket();
}
//让这个客户端反复从控制台读取用户输入的内容 把这个这个内容构造称UDP请求 发给服务器 再读取服务器返回的UDP响应
//最终再显示客户端的屏幕上
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
while (true){
//1.从控制台读取用户输入的内容
System.out.println("->");
String request = scanner.next();
//2.构造请求对象 并发给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
//此处需要的是InetAddress对象使用的是InerAddress静态方法 getByName来构造对象
socket.send(requestPacket);
//3.读取服务器响应 并解析出响应内容
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String respose = new String(responsePacket.getData(),0 ,responsePacket.getLength());
//4.显示到屏幕上
System.out.println(respose);
}
}
public static void main(String[] args) throws IOException {
//客户端接下来要访问9090这个端口
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9527);
client.start();
}
}


此时启动多个客户端 多个客户端也可以被服务器应对
再IDEA启动多个客户端


这样就能启动多个客户端

对于服务器DatagramSocket 不关闭问题
对于整个程序只有一个socket对象不是频繁创建 整个对象的生命周期非常长 随着整个程序 socket就需要保存打开状态 socket对象对应socket文件 文件描述符 最主要的目标是为了释放文件描述符 才关闭socket对象的 进程结束 九八pcb回收 里面的文件描述符表也就都销毁了(仅限于只有一个socket对象 并且生命周期是跟随整个进程的)
如果是由多个socket对象 socket对象生命周期短一定要close
- DatagramPacket
表示了一个UDP数据报
代表了系统中设定的UDP数据包的二进制结构
自己实现一个翻译服务器
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//翻译服务器
public class UdpDicServer extends UdpEchoServer{
private Map<String,String> dict = new HashMap<>();
public UdpDicServer(int port) throws SocketException {
super(port);
dict.put("cat","猫");
dict.put("dog","狗");
//可以在这里添加许多翻译单词
}
//是要复用父类的方法但又要有所调整
public String process(String request){
//把对应的单词翻译 给返回回去
return dict.getOrDefault(request,"该词没有对应的翻译");
}
public static void main(String[] args) throws IOException {
UdpDicServer server = new UdpDicServer(9527);
//start 不需要重新写 直接复用之前的start
server.start();
}
}
我们新建一个类 继承之前的UdpEchoServer类 然后使用hashmap存储对应的翻译 再对原本process方法进行修改若map中存在翻译的单词 就返回对应的键值 若没有就返回"该词没有对应的翻译" 最后调用客户端有
TCP协议
比UDP更重要 用的更多的协议
TCP提供的api也是主要有两个类
ServerSocket(给服务器使用的socket)
Socket(既给服务器又给客户端)
TCP是字节流 使用一个tcp数据报就是一个字节数组 byte[] 不用特意做成一个报
ServerSocket API
ServerSocket 构造方法
| 方法签名 | 方法说明 |
|---|---|
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法
| 方法签名 | 方法说明 |
|---|---|
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
Socket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| Socket(String host, intport) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
| 方法签名 | 方法说明 |
|---|---|
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP版本的回显服务器
package network;
import java.io.IOException;
import java.net.ServerSocket;
public class TCPEchoServer {
private ServerSocket serverSocket = null;
//操作绑定端口号
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start(){
System.out.println("服务器启动!");
while (true){
}
}
}
进入循环之后 要做的事情不是读取客户端的请求 而是先处理客户端的连接
一个服务器 要对应很多客户端服务器内核里有很多客户端的连接
虽然内核中的连接很多但是在应用程序中 还是得一个一个处理
内核中的连接就像是一个 “代办事项” 这些待办事项在一个队列数据结构中 应用程序就需要一个一个完成这些任务 就需要先取任务 就要用到 **serverSocket.accept()**方法

TCP程序涉及到两种写法
- 一个连接只传输一次请求和响应 短连接
- 一个连接种 可以传输多次请求和响应 长连接
服务器代码
package network;
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;
public class TCPEchoServer {
private ServerSocket serverSocket = null;
private ExecutorService service = Executors.newCachedThreadPool();
//操作绑定端口号
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了 类似于生产者消费 模型
//当服务器执行到accept的时候此时客户端还没来accept就会阻塞直到客户端连接成功为止
//单个线程不太方便完成多个客户端
/* Thread t = new Thread(()->{
processConnection(clientSocket);
});
t.start();*/
//使用线程池解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
//通过这个方法来处理一个连接的逻辑
private void processConnection(Socket clientSocket){
System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//根据请求计算响应 返回响应分三步走
//Scoket对象内部包含了两个字节流对象 可以把这连个字节流对象获取到 完成后续的构造
try(InputStream inputStream = clientSocket.getInputStream();//耳机
OutputStream outputStream = clientSocket.getOutputStream()){//麦克风
//一次连接中 可能会涉及多次响应请求
while (true){
//1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//读取完毕 客户端下线
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//客户端发过来的请求得是文本数据 同时还得带有空白符作为分割
String request = scanner.next();
//next就是读取数据一直读到空白符结束包括不限于 换行 回车 翻译符等
//2.根据请求计算响应
String response = proess(request);
//3.把响应写回给客户端 把OutputStream 使用PrinterWriter 包裹一下 方便进行发数据
PrintWriter writer = new PrintWriter(outputStream);
//使用PrintWriter的println方法把响应返回给客户端
//此处使用println而不是print就是为了在结尾加上\n 方便客户端读取响应 使用Scanner.next读取
writer.println(response);
//刷新缓冲区
writer.flush();
//IO操作是有开销的 相比较于内存进行的IO次数越多 程序速度就越慢 使用一块内存作为缓冲区 写数据的时候先写到缓存区中 攒一波数据 统一进行io
//手动刷新是确保数据是真的通过网卡发出去了 而不是残留在内存缓冲区中
//日志 打印当前请求详情
System.out.printf("[%s:$d] req:%s, resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String proess(String request){
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer server = new TCPEchoServer(9527);
server.start();
}
}
客户端编写
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//客户端代码
public class TCPEchoClient {
private Socket socket = null;
//完成了TCP连接的建立
public TCPEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动");
Scanner scannerConsloe = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while (true){
//1.从控制台输入字符串
System.out.print("->");
String request = scannerConsloe.next();
//2.把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
//使用println带上换行 后续服务器读取请求 就可以使用scanner.next了
printWriter.println(request);
printWriter.flush();
//3. 从服务器读取响应
Scanner scannerNetwork = new Scanner(inputStream);
String response = scannerNetwork.next();
//4. 把响应打印出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1",9527);
tcpEchoClient.start();
}
}

被折叠的 条评论
为什么被折叠?



