目录
网络编程
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(); } }
注意:
在进行运行测试时应该先启动服务器再启动客户端