目录
网络编程
IP地址:使用一个32位4字节数字表示地址(一般来说会把IP地址给表示成4个0-255之间的十进制数字,并使用3个点进行分隔)
端口号:区分一个主机上不同的应用程序,端口号是一个整数(2个字节,0-65535)(一个端口号只能被一个程序绑定,但是一个程序可以绑定多个端口)
//0一般不使用,1-1023这个范围的端口号系统留作特殊用途,写的程序不应该占用
封装和分用
描述了网络通信过程中基本的数据传输流程
//一个数据报=报头+载荷(字符串拼接)
1.应用层:根据约定的应用层协议来生成应用层数据报,通过操作系统的api把数据交给传输层(具体是用几个字段,字段的顺序如何,使用什么符合分隔都是可以灵活调整的)
2.传输层:在应用层数据报的基础上拼接上传输层的报头,变成传输层的数据报(传输层典型的协议,TUP,UDP)(包含源端口和目的端口),传输层数据报搞好之后这个数据又会进一步交给网络层
3.网络层
网络层最主要的协议是IP协议(包含源IP和目的IP)
再交给数据链路层进一步打包
4.数据链路层
最主要的协议是以太网(报头中包含源mac地址和目的mac地址)//此处会比之前多一个加报尾的操作
5.物理层
把上述数据转化成2进制的01序列后通过光信号/电信号进行传输
//数据发送出去之后就会经过一系列的交换机和路由器进行转发,A和B一般来说不是直接网线连接的,中间还要经过很多的交换机/路由器设备进行转发,当数据到达B这边之后,B就要针对上述数据进行"分用"(针对上述数据报进行层层的解析)
数据报在网络中间还会经历一定的转发过程;如果经过路由器就会封装分用到网络层,路由器解析到网络层拿到IP地址再决定进一步如何运输,下一步传输的时候又会重新经过数据链路层和物理层的封装;如果经过交换机就会封装分用到数据链路层
通过Socket API进行应用层和传输层之间的交互
传输层提供的网络协议主要是TCP和UDP,这两个协议的特性(工作原理)差异很大,导致使用这两种协议进行网络编程时也存在一定差别,所以系统分别提供了两套API
//TCP和UDP的区别
(1)TCP是有连接的,UDP是无连接的
(连接是抽象的概念,计算机中这种抽象的连接是很常见的,此处的连接本质上是建立连接的双方各自保存对方的信息)
//TCP要想通信就得先建立连接(得对方同意才能通信),UDP想要通信直接发送数据即可(UDP不保存对方的信息,但是我们调用UDP的socket api的时候要把对方的位置啥的给传过去)
(2)TCP是可靠传输的,UDP是不可靠传输的
//可靠传输指的是A在传送消息失败时能感知到,就可以在发送失败的时候采取一定的措施(尝试重传之类的)可靠传输的代价是机制更复杂以及传输效率会降低
(3)TCP是面向字节流的,UDP是面向数据报
(4)TCP和UDP都是全双工的
//一个信道允许双向通信就是全双工(代码中使用一个socket对象就可以发送数据也能接受数据),单向通信就是半双工
网络通信数据的基本单位:
【1】数据报(Datagram)【2】数据包(Packet)【3】数据帧(Frame)【4】数据段(Segment)
UDP的socket api如何使用
【1】DatagramSocket
void receive(DatagramPacket p)
void send(DatagramPacket p)
void close()
//socket本质上是一种特殊的文件,属于是把“网卡”这个设备抽象成了文件,做到了把网络通信和文件操作给统一了
【2】DatagramPacket
创建时得指定一块内存空间DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
//使用这个类来表示一个UDP数据报,UDP是面向数据报的,每次进行传输都要以UDP数据报作为基本单位
服务器和客户端都需要创建socket对象,但是服务器的socket一般要显式的指定一个端口号,而客户端的socket一般不能显式指定(不显式指定,此时系统会自动分配一个随机的端口)
【服务器端代码】
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer{
//创建一个DatagramSocket对象,后续操作网卡的基础
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
//这么写就是手动指定端口
socket=new DatagramSocket(port);
//这么写就是系统自动分配端口
//socket=new DatagramSocket();
}
public void start() throws IOException {
//通过这个方法来启动服务器
System.out.println("服务器启动");
//一个服务器程序中,经常能看到while true这样的代码
while(true){
//1.读取请求并解析
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//当前完成receive之后,数据是以二进制的形式存储到DatagramPacket中了
//要想能够把这里的数据给显示出来,还需要把这个二进制数据给转成字符串
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应(一般的服务器都会经历的过程)
//由于此处是回显服务器,请求是啥样,响应就是啥样
String response=process(request);
//3.把响应写回到客户端
//搞一个响应对象DatagramPacket,往DatagramPacket里构造刚才的数据,再通过send返回
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印一个日志,把这次数据交互的详情打印出来
System.out.printf("[%s:%d] req=%s,resp=%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),requese,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException{
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
【客户端代码】
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient{
private DatagramSocket socket=null;
private String serverIp="";
private int serverPort=0;
public UdpEchoClient(String ip,int port) throws SocketException {
//创建这个对象时不能手动指定端口
socket=new DatagramSocket();
//由于UDP自身不会持有对端的信息,就需要在应用程序里把对端的情况给记录下来
//这里主要记录对端的ip和端口
serverIp=ip;
serverPort=port;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner=new Scanner(System.in);
while(true){
//1.从控制台读取数据作为请求
System.out.print("->");
String request=scanner.next();
//2.把请求内容构造出DatagramPacket对象发给服务器
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),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);
client.start();
}
}
TCP的socket api
(TCP是字节流的,传输的基本单位是byte)
【1】ServerSocket
给服务器使用的类,使用这个类来绑定端口号
【2】Socket
既会给服务器用,又会给客户端用
【服务器端代码】
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;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException{
System.out.println("服务器启动!");
ExecutorService service= Executors.newCachedThreadPool();
while(true){
//通过accept方法,把内核中已经建立好的连接拿到应用程序中
//建立连接的细节流程都是内核自动完成的,应用程序只需要“捡现成”的
Socket clientSocket=serverSocket.accept();
//此处不应该直接调用processConnection,会导致服务器不能处理多个客户端
//创建新的线程来用是更合理的做法
// 这种做法可行,但是不够好
// Thread t=new Thread(()->{
//processConnection(clientSocket);
//});
//t.start();
//更好一点的办法是使用线程池
service.submit(new Runnable(){
@Override
public void run(){
processConnection(clientSocket);
}
});
//还有一些其他手段可以来处理线程较多的情况:协程、IO多路复用、IO多路转接
}
}
//通过这个方法来处理当前的连接
public void processConnection(Socket clientSocket){
//进入方法,先打印一个日志,表示当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//接下来进行数据的交互
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
//使用try()方式,避免后续用完了流对象忘记关闭
//由于客户端发来的数据可能是“多条数据”,针对多条数据就应该循环处理
while(true){
Scanner scanner=new Scanner(inputStream);
if(!scanner.hasNext()){
//连接断开了循环就应该结束
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//1.读取请求并解析,此处就以next来作为读取请求的方式,next的规则就是读到“空白符”就返回
String request=scanner.next();
//2.根据请求计算响应
String response=process(request);
//3.把响应写回到客户端
//也可以把String转成字节数组,写入到OutputStream
//也可以使用PrintWriter把OutputStream包裹一下,来写入字符串
PrintWriter printWriter=new PrintWriter(outputStream);
//此处的println不是打印到控制台了,而是写入到outputStream对应的流对象中,也就是写入到clientSocket里面
//自然这个数据也就通过网络发送出去了(发给当前这个连接的另外一端)
//此处使用println带有\n也是为了后续客户端这边可以使用scanner.next来读取数据
printWriter.println(response);
//此处还要记得有个操作,刷新缓冲区,如果没有刷新操作可能数据仍然是在内存中而没有被写入网卡
printWriter.flush();
//4.打印一下这次请求交互过程的内容
System.out.printf("[%s:%d] req=%s,resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
//在这个地方进行clientSocket的关闭
//processConnection就是在处理一个连接,这个方法执行完毕,这个连接也就处理完了
clientSocket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
public String process(String request){
//此处也是写的回显服务器,响应和请求是一样的
return request;
}
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.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient{
private Socket socket=null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//需要在创建Socket的同时和服务器“建立连接”,此时就得告诉Socket服务器在哪里
//具体建立连接的细节不需要利用代码手动干预,是内核自动负责的
//当new这个对象的时候,操作系统内核就开始进行“三次握手”具体细节完成建立连接的过程
socket=new Socket(serverIp,serverPort);
}
public void start(){
//tcp的客户端行为和udp的客户端差不多
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
PrintWriter writer=new PrintWriter(outputStream);
Scanner scannerNetwork=new Scanner(inputStream);
while(true){
//1.从控制台读取用户输入的内容
System.out.print("-> ");
String request=scanner.next();
//2.把字符串作为请求发送给服务器
//这里使用println是为了让请求后面带上换行
//也就是和服务器读取请求的scanner.next呼应
writer.println(request);
writer.flush();
//3.从服务器读取响应
String response=scannerNetwork.next();
//4.把响应显示到界面上
System.out.println(response);
}
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
注意:
在进行运行测试时应该先启动服务器再启动客户端