网络编程
当数据交给上一层的时候,是由哪个协议负责进行解析呢??
比如,数据链路层=>网络层,交给IPv4解析?IPv6解析?网终层=>传输层交绘TCP解析还是IDP2。
socket=>操作系统提供的网络编程的API就称为socketapi(插槽),可以认为是“网络编程api”的统称
1.流式套接字=>给TCP使用的
TCP的特点:有连接,可靠传输,面向字节流,,全双工。
2.数据报套接字=>给UDP使用的
UDP1的特点:无连接,不可靠传输,面向数据报,全双工
TCP和UDP都是传输层协议,都是给应用层提供服务的
有连接vs无连接
连接即是要建立连接的双方各自保存对方的信息。
有连接
就像打电话,只有对方允许后才可以接听,也可以选择直接挂掉
通信双方保存对方的信息
无连接
就像发短信/发微信,不需要“先接通”,直接上来就发
通信双方不需要保存对方的信息
可靠传输vs不可靠传输
可靠传输
可靠!=安全(传输的数据是否容易被黑客截获掉,一日被截获之后是否会造成严重的影响)
尽力做到数据达到对象进行传输。
在网络信息通信的过程中,A给B传输10个数据报,B实际上只收到9个。
由于网络环境太复杂了,A传输给B,中间可能会经历很多的交换机和路由器,进行转发这些交换机和路由器,也不只是转发你的数据,要转发很多数据。但是如果数据进行堵塞的时候,数据会直接被丢弃掉。
但是如果遇到物理阻断的时候就会导致数据无法传输。
不可靠传输
不可靠传输.传输数据的时候,压根不关心,对方是否收到.发了就完了。
区别:效率问题
面向字节流VS面向数据报
面向字节流
文件操作,就是字节流的字节流,比喻成水流一样.读写操作非常灵活
面向数据报
传输数据的基本单位,是一个个的UDP数据报一次读写,只能读写一个完整的UDP数据报,不能搞半个数据报。
网络传输数据的基本单位
数据报 Datagram UDP
数据段 Segmen tTCP
数据包 Packet IP
数据帧 Frame 数据链路层
全双工VS半双工
全双工:一条链路,能够进行双向通信(TCP,UDP都是全双工)
创建socket对象,既可以读(接受)也可以写(发送)
半双工:一条链路,只能进行单向通信
"C语言中学到的管道”pipe,就属于半双工)
UDP
socketapi都是系统提供的.(不同系统,提供的api是不一样)Java中对于系统的这些api进一步的封装了.
1.DatagramSocket
DatagramSocket就是对于操作系统的socket(系统中的socket,可以理解成是一种文件(针对socket文件的读写操作就相当于针对网卡这个硬件设备进行读写)
文件是一个广义的概念~)
概念的封装
此处,DatagramSocket就可以视为是“操作网卡”的遥控器(句柄)针对这个对象进行读写操作,就是在针对网卡进行读写操作。
2.DatagramPacket
一个DatagramPacket对象,就相当于一个UDP数据报一次发送/一次接受,就是传输了一个DatagramPacket对象。
回显(Echo):正常的服务器,你给他发不同的请求,会返回不同的响应。
网络编程,本质上也是IO操作
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws IOException {
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);
//将java代码中处理可以把上述数据中的二进制数据,拿出来,构成String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应
String response=process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.println(responsePacket.getAddress());
}
}
//回显服务器
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
udpEchoServer.start();
}
}
客户端是主动的一方服务器是被动的一方
服务器程序,需要在程序启动的时候,把端口号确定下来
服务器端口号的要求:
1.端口号是合法的1-65535
2.注意端口号不能和别的进程使用的端口号冲突。
写代码的时候,很少会有这样的操作,持有数据的空间,需要额外的创建(但是socket编程原生api,就都是这种风格,这样的风格也延续到java中了)
服务器服务器的ip和端口得是固定的,不能老变。
客户端也需要端口号!!
在进行一次通信的过程中,需要知道至少4个核心指标
:1)源IP发件人地址
2)源端口发件人电话
3)目的IP收件人地址
4)目的端口收件人电话
构造socket对象的时候,没有指定端口号,没指定不代表没有而是操作系统,自动分配了一个空闲的(不和别人冲突)的端口号过来了这个自动分配的端口号,每次重新启动程序都可能不一样。
为啥服务器需要有固定端口号,而客户端就需要让系统自动分配?反过来行不行??
1)服务器要有固定端口号,是因为,客户端需要主动给服务器发请求如果服务器端口号不是固定的,(假设是每次都变,此时客户端就不知道请求发给谁了)
2)客户端为啥要系统自动分配,指定固定的端口号行不行呢?
如果就给客户端指定固定的端口号,是不行的!!因为指定的固定端口号,是可能会和客户端所在电脑上的其他的程序,冲突的一旦端口冲突,就会导致你的程序启动不了了。
这里应用的是回显服务器(请求是啥,响应就是啥)
一个正常的服务器,要做三个事情
1.读取请求并解析
2.根据请求,计算响应
3.把响应写回到客户端
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;
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
// 这俩信息需要额外记录下来, 以备后续使用.
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("请输入要发送的请求: ");
// 1. 从控制台读取用户输入
String request = scanner.next();
// 2. 构造请求并发送
// 构造请求数据报的时候, 不光要有数据, 还要有 "目标"
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 显示响应到控制台上.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("139.155.74.81", 9090);
client.start();
}
}
“127.0.0.1”服务器和客户端在一个主机上此时就固定写这个ip地址(环回ip,系统提供的特殊的ip)
9090:此处写的是服务器在创建建socket对象时,指定的端口号。
getBytes().length();和.length()方法的区别:
前者是得到字节数组然后通过字节十足的长度,这里的长度单位是“字节数”,我们可以看到request是String类型所以需要取字符
后者是是长度单位是字符数,本身就是字符类型所以不需要取字符。
如果不转换会发生那些问题呢,如果字符串是英文字母或者数字的话问题不大,但是在字符串中出现了字符或者汉字来说,就会导致长度变小,例:java中String的字节大多都是三个字符,如果不改变就会导致3倍差距。
客户端流程: 服务器流程
1.从控制台读取字符串
2.把字符串发送给服务器-----
| 1.读取请求并解析
ms级别 2.根据请求计算响应
| 3.把响应写回到客户端
3.从服务器读取到响应-------- (如果没有收到服务器的响应会阻塞)
4.把响应打印到控制台上
先运行服务器,后运行客户端
如果不能开多个客户端就可以这样操作
当两个电脑在同一个局域网下,才可以进行互联。
主要是因为电脑的ip不行,我们用的是私网ip但是另一个是公网ip。(可以通过租服务器解决)。
私网ip有
1.10.
2.172.16-172.31.
3.192.168.
import java.io.IOException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String, String> dict = null;
public UdpDictServer(int port) throws IOException {
super(port);
dict = new HashMap<>();
dict.put("hello", "world");
dict.put("123", "456");
dict.put("12345", "上山打老虎");
}
@Override
public String process(String message) {
return dict.getOrDefault(message, "没有查到");
}
}
可以通过继承已有的方法来实现process的方法。
不用调用close 的原因是因为scocket对象不能释放,结束了就会自动释放了。
TCP
TCP的socket api
1.ServerSocke
t专门给服务器用的ServerSocket(intport)
Socketaccept()接听操作
void close()
Socket
Socket(String host, int port) 构造方法本身,就能够和指定的服务器,简历连接(拨号的过程)
InputStream getlnputStream() 获取到socket内部持有的流对象
OutputStream getOutputStream() 也是通过InputStream和OutputStream来操作的,通过read和write来进行。
InetAddress getlnetAddress() 获取地址
private serverSocket=null;
Socket clientSocket =serverSocket.accept();
服务器一启动,就会立即执行到这里如果客户端没有连接过来,accept也会产生阻塞直到说有客户端真的连接上来了!
socket api
1)ServerSocket用于在服务器端使用的.(揽客)
2)Socket用于服务器和客户端,来进行通信 getInputStream getOutputStream
服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
Socket socket = serverSocket.accept();
processConnection(socket);
}
}
public void processConnection(Socket socket) {
System.out.printf("[%s:%d]客户端上线!\n",socket.getInetAddress(),socket.getPort());
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
String response=process(request);
outputStream.write(response.getBytes()); ;
System.out.println(response);
}
}catch ( Exception e ){
e.printStackTrace();
}
}
private String process(String request) {
return request+"\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverport) throws IOException {
socket = new Socket(serverIp, serverport);
}
public void start() throws IOException {
System.out.println("客户端");
try (InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
Scanner scanner = new Scanner(System.in)) {
Scanner scannerNetwork = new Scanner(in);
while (true) {
System.out.println("请输入发送的数据");
String request = scanner.next();
out.write(request.getBytes());
out.write('\n'); // Add newline delimiter
out.flush(); // Flush the output stream
if (request.equals("exit")) {
System.out.println("客户端下线");
break;
}
String response = scannerNetwork.nextLine();
System.out.println("服务器回复: " + response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
打开多个客户端的时候会出现两个客户端无法同时访问一个服务器,所以要用到多线程或者更进一步的线程池.
String request = scanner.next();
这个读取方式,就是会读到“空白符”才会读取完毕,使用next读取数据,如果数据中不带有\n等分隔符的话此时next就会一直阻塞~~
如果直接按照read的方式来读,读出来的是byte口还需要转成String
Scanner ,直接读出来就是String类型。
运行顺序
服务端的执行操作
while(true) {
Socket socket = serverSocket.accept();
processConnection(socket);
}
这个东西在processConnection中使用之后没有进行close这个是不科学的!!
服务器会对应多个客户端,每个客户端都有一个对应的clientSocket.
如果用完了不关闭,就会使当前clientSocket对应的文件描述符得不到释放,引起了文件资源泄露
所以要加入
finally {
socket.close();
}
.
客户端在发送数据的时候,务必要在每个请求的末尾,填上空白符,比如填上\n
服务器是无法同时给多个客户端提供服务端,如下图所示
只有当进程客户端1结束后,进程客户端2才会开始。输入的信息也会随之出现。
这是由于第一个客户端连上之后,此时accept就返回了,进入到processConnection方法,就在方法内部的while循环,开始循环起来了.,第二个客户端又来了之后,此时,没有办法执行到第二次accept的(第二个客户端,给服务器打电话,服务器一直没接听!!)。所以要用到多线程。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
Socket socket = serverSocket.accept();
Thread t=new Thread(()->{
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
public void processConnection(Socket socket) throws IOException {
System.out.printf("[%s:%d]客户端上线!\n",socket.getInetAddress(),socket.getPort());
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
String response=process(request);
outputStream.write(response.getBytes()); ;
System.out.println(response);
}
}catch ( Exception e ){
e.printStackTrace();
}finally {
socket.close();
}
}
private String process(String request) {
return request+"\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
这样就解决问题了
也可以用线程池方法
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Executable;
import java.net.ServerSocket;
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 TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService pool= Executors.newCachedThreadPool();
while(true) {
Socket socket = serverSocket.accept();
// Thread t=new Thread(()->{
// try {
// processConnection(socket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
// t.start();
pool.submit(new Runnable (){
public void run() {
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
public void processConnection(Socket socket) throws IOException {
System.out.printf("[%s:%d]客户端上线!\n",socket.getInetAddress(),socket.getPort());
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d]客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
String response=process(request);
outputStream.write(response.getBytes()); ;
System.out.println(response);
}
}catch ( Exception e ){
e.printStackTrace();
}finally {
socket.close();
}
}
private String process(String request) {
return request+"\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
一个计算机来说,同时创建出几百个线程,就已经压力很大了,搞个几万个线程,你的机器大概率要卡的动不了~~
这里核心解决方案,IO多路复用+多个服务器(分布式系统)
使用一个线程,就管理多个socket,这些socket往往不会是同时有数据需要处理而是同一时刻,只有少数socket需要读取数据(同时管理1w个socket,同一时刻,只有100个socket需要读取数据)。有点像分时复用。
当前写的tcpserver和client这里,就涉及到三种socket
1)服务器ServerSocket
2)服务器Socket(通过这个Socket和客户端提供交互能力)
3)客户端Socket(通过这个Socket和服务器进行交互)
13都是生命周期跟随整个进程程序只要在运行,就需要这个socket不能提前close的随着进程结束,这些socket自然释放,也不需要手动写close.
12中服务器这边会有多个这样的socket每个客户端都有一个对应的socket这个socket在客户端断开连接之后,就不再使用了就需要关闭掉~
TCP/IP五层协议
应用层传输层网络层数据链路层物理层。
应用层和应用程序直接相关。
自定义应用层协议,本质上就是对传输的数据做出约定~~
1)约定传输的数据要有哪些信息
2)传输的数据要遵守啥样的格式
1)信息
请求:用户的id,所在的位置(经纬度)
响应:商家列表每个元素包含:商家名称商家图片商家评分商家的简介
2)确定数据的格式
上述的数据,都属于“结构化数据”(通过结构体来表示的数据)网络上传输的是字符串(二进制字符串),就需要对数据进行序列化序列化的方式是有很多种的~~