目录
一、什么是网络编程?
二、那么在Java中能调用C语言的函数吗?
三、操作系统提供的socket API主要有两类(实际上不止两类)
1.流套接字(底层使用TCP协议)
TCP协议的特点:
2.数据报套接字(底层使用UDP协议)
UDP协议的特点:
UDP socket中有两个核心的类:
四、正常的客户端/服务器的通信流程:
五、服务器的代码:
六、客户端和服务器
七、客户端的代码:
总结
一、什么是网络编程?
通过代码来控制,让两个主机的进程之间能够进行数据交互。
例如我用微信发送一条消息,这个消息通过我电脑上的QQ客户端进程,先发送给了腾讯的服务器(对应的服务器进程),再由腾讯的服务器进程把这个消息发给对方电脑的QQ进程。
操作系统把网络编程的一些相关操作(管理网卡)封装起来了,提供了一组API供程序员来调用。
进行网络编程的核心就是通过代码操作网卡这个硬件设备。
操作系统对于网卡进行了抽象,进程想去操作网卡的时候,就会打开一个“socket文件”,通过读写这个socket文件,就能读写网卡了
操作系统提供的这组API,叫做socket(套接字) API,是C语言风格的接口,在Java中是不能直接使用的。JDK针对C语言这里的socket API进行了封装。
在Java标准库有一组类,这组类能够让我们完成网络编程,这组类本质上调用的是操作系统提供的socket API。
二、那么在Java中能调用C语言的函数吗?
答案是可以,这就是跨语言调用,不同的语言之间很多都可以互相调用,而实现跨语言调用核心原理在于了解对应语言的ABI(二进制编程接口)。
三、操作系统提供的socket API主要有两类(实际上不止两类)
1.流套接字(底层使用TCP协议)
TCP协议的特点:
第一、有连接
第二、可靠传输
第三、面向字节流
第四、全双工
2.数据报套接字(底层使用UDP协议)
UDP协议的特点:
第一、无连接
第二、不可靠传输
第三、面向数据报
第四、全双工
TCP协议和UDP协议都是传输层的协议,socket API属于传输层
有连接的意思就好比打电话,你拨给对方,只有对方接听了,你俩才能交流,这个交流是实时的。
无连接的意思好比发微信,只要两个人是好友,那么发消息的一方不用管对方此时想不想接收、在不在,总之,消息都可以发送过去,只顾自己发消息成功就好了,不用管对方的状态。
可靠传输:这里千万别理解成消息能够100%发送成功(被对方收到),这是绝对不可能的,因为技术再怎么牛,也禁不住降维打击:拔网线。还有也不能理解成发送消息100%安全。这里的正确理解是发送消息的一方知道接收信息的一方是不是收到消息了。这个可以结合带你电话来理解。
不可靠传输:这个理解和可靠传输相反,即发送消息的一方不知道接收信息的一方是不是收到消息了。这个可以结合发微信来理解。
千万别认为TCP比UDP更安全,可靠性不等于安全性。
面向字节流:例如要发送100个字节,可以一次发一个字节,重复100次,也可以一次发10个字节,重复10次......,可以非常灵活的完成这里的发送,接受也是同理。
TCP是面向字节流的,文件读写也是面向字节流的。
面向数据报:以一个一个的数据报为基本单位(每个数据报的大小不同的协议里面有不同的约定),发送的时候,一次至少发一个数据报,如果一次尝试发一个半数据报,实际上只能发出去一个;接收的时候,一次至少收一个数据报,如果一次尝试收一个半数据报,实际上只能收到一个,剩下的半个就没了。
全双工:其实就是双向通信,即A和B可以同时向对方发送接受数据
这个可以理解为A和B之间有两根水管,一根由A发送水给B,一根由B发送水给A,互不干扰。
TCP和UDP都是全双工。
半双工:单向通信,要么A给B发,要么B给A发,不能同时发。
这个可以理解为A和B之间只有一根水管,要么由A给B发送水,要么B给A发送水,二者是不能同时发的。
应用层对应的是应用程序,我们在应用程序中使用socket API,传输层、网络层、数据链路层、物理层对应的是操作系统和硬件
socket API之所以属于传输层是因为传输层离应用层最近
UDP socket中有两个核心的类:
1.DatagramSocket 描述一个socket对象
Java标准库中的DatagramSocket对象表示一个socket文件。
这里面涉及到一个核心方法receive(),它的作用是接收数据,具体的执行过程是如果没有数据过来,那么receive()就阻塞等待,如果有数据了,receive()就返回一个DatagramSocket对象。
还有个send()方法,作用是发送数据,以DatagramPacket为单位进行发送。
操作系统提供的网络编程API叫做socket API,socket API中涉及到一个核心概念socket,socket本质上是一个文件描述符。
文件描述符的解释在“怎么读文件的资源泄露中详细介绍过”,我们这里再次说一下
某个进程被创建出来,进程就会对应一个PCB,PCB中包含了一个文件描述符表,每次打开一个文件,就会在文件描述表中分配一个表项,文件描述符表类似于一个数组,数组的下标就是文件描述符,数组的元素是一个内核结构struct file,struct file是C语言中的结构体。
另外,我们在前面还提到过“一切皆文件”的思想,操作系统在管理硬件设备和一些软件资源的时候,为了能够风格统一,于是就都用文件的方式来管理
比如:普通文件、键盘(标准输入文件)、显示器(标准输出文件),网卡也是一个硬件设备,操作系统也是用文件来管理网卡,此处用来表示网卡设备的文件,也就死socket文件。
而要想操作网卡,就首先需要创建出一个socket文件,通过读写这个socket文件的方式就可以操作网卡了。这里的socket文件就好像是遥控器,而网卡就好比电视机,我们通过遥控器来控制电视。
2.DatagramPacket 描述一个UDP数据报
UDP是面向数据报的,发送和接收数据就是以DatagramPacket对象为单位进行的。
解决了怎么发送和怎么接收的问题后,问题又来了,我们需要知道发送的时候,目标在哪里,接收的时候,这个数据从哪里来
解决这个问题的答案就是我们之前学过的ip地址和端口号,由ip地址确定主机位置,端口号确定进程。而ip地址和端口号可以由InetSocketAddress类来表示。
基于UDP的socket来写一个非常非常简单的回显程序,即回显客户端+回显服务器
回显的意思是A给B说啥,B就回应啥(复读机)
回显程序本身没意义,写这个程序主要是为了熟悉Socket API具体的应用。
四、正常的客户端/服务器的通信流程:
客户端一启动的时候,就需要知道服务器的ip和端口号
而服务器一启动的时候,是无法知道客户端的ip和端口号的,只有客户端的请求到了,服务器才能知道对应客户端的ip和端口号。
因此服务器和客户端的关系是不对等的
这个可以通过客人于餐馆的例子来说明
对于餐馆(好比服务器)来说,今天有多少客人,它是不知道的
对于客人(好比客户端)来说,要想吃饭,就必须知道餐馆的位置
五、服务器的代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {private DatagramSocket socket = null;//port表示端口号//服务器在启动的时候需要绑定一个端口号//收到数据的时候,会根据这个端口号决定把这个数据交给哪个进程//虽然此处port写的类型是int,但实际上端口号是一个两个字节的无符号整数//范围是0~65535public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}//通过这个方法来启动服务器public void start() throws IOException {System.out.println("服务器启动");//服务器一般都是7*24小时运行while (true) {//1、读取请求。当前服务器不知道客户端啥时候发来请求,receive方法也会阻塞// 如果真的有请求过来了,此时receive就会返回// 参数DatagramPacket是一个输出型参数,socket中读到的数据会设置到这个对象的参数中// DatagramPacket在构造的时候,需要指定一个缓冲区(就是一段内存空间,通常使用byte[])DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);socket.receive(requestPacket);//把socketPacket对象里面的内容取出来,作为一个字符串String request = new String(requestPacket.getData(),0, requestPacket.getLength());//2、根据请求计算响应String response = process(request);//3、把响应写回到客户端,这时候也需要构造一个DatagramPacket// 此处给DatagramPacket中设置的长度,必须是字节的个数// 如果直接取response.length(),此处得到的是字符串的长度,也就是字符的个数// 当前的responsePacket在构造的时候,还需要指定这个包要发给谁// 其实发送给的目标就是发送请求的那一方DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);//4、日志打印//%s 表示要打印一个字符串//%d 表示要打印一个有符号的十进制的整数String log = String.format("[%s:%d] request: %s; response: %s",requestPacket.getAddress(),requestPacket.getPort(),request,response);System.out.println(log);}}//此处process方法负责的功能就是根据请求计算响应//由于当前是一个回显服务器,就是把客户端发的请求直接返回去即可private String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}
六、客户端和服务器
由于是客户端先给服务器发送请求
因此客户端就需要先知道服务器的ip和端口
服务器如果收到了客户端的请求,那么就知道客户端的ip和端口了。
七、客户端的代码:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;//参数serverIp和serverPort,是服务器的ip和端口号public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.serverIp = serverIp;this.serverPort = serverPort;this.socket = new DatagramSocket();//这里为什么没有端口号这个参数呢?//客户端在构造这个Socket对象的时候,不需要指定端口号,尤其是不能把serverPort指定进去//举一个例子//A(客户端)给B(服务器)发一个快递//A首先需要知道B的收货地址(serverIp)和收货电话(serverPort)//然后在快递包裹上写上收件人的信息以及发件人的发货地址(ip)和发货电话(port)//而上述DatagramSocket()构造方法中传入的port,这是指定自己的端口//如果A在DatagramSocket()这个构造方法中传入了serverPort,这就是相当于把自己发件人的电话写成了收件人电话//如果当前DatagramSocket()构造方法中没有指定端口的话,操作系统会分配一个空闲的端口号给客户端使用//为啥客户端的端口号可以随机生成,而服务器这里就必须手动指定呢?//客户端需要明确知道服务器的端口号是啥,才能通信//这就好比A给B发快递,必须要知道B的电话是啥,要不然咋发//而B不必知道A的电话//刚才构造Socket时,我们都是围绕端口来操作的,那么ip呢?//答案是服务器这边的ip相当于被自动设置为本机的ip//客户端这边的ip也自动设置为本机的ip,但是需要手动指定一下服务器的ip.}public void start() throws IOException {Scanner scanner = new Scanner(System.in);while (true) {//1.从标准输入读入一个数据System.out.println("->");String request = scanner.nextLine();if (request.equals("exit")) {System.out.println("exit");return;}//2.把字符串构造成一个UDP请求,并发送数据// 这个DatagramPacket中,既要包含具体的数据,又要包含这个数据要发给谁DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);socket.send(requestPacket);//3.尝试从缓冲区读取响应// DatagramPacket在构造的时候,需要指定一个缓冲区(就是一段内存空间,通常使用byte[])DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);socket.receive(responsePacket);String response = new String(responsePacket.getData(),0, responsePacket.getLength());//4.显式这个结果String log = String.format("req: %s, resp: %s",request,response);System.out.println(log);}}public static void main(String[] args) throws IOException {//127.0.0.1是环回ip,环回ip表示的是主机本身//当前客户端和服务器在同一台主机上//所以在客户端写的服务器ip是127.0.0.1//如果在不同主机上,就需要写成对应服务器的ipUdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);client.start();}}
把服务器的程序运行,然后把客户端的程序运行,输入数据得到的结果是
客户端的界面:
服务器的界面:
当使用同一台主机部署客户端和服务器的时候,此时虽然是网络的那套逻辑,但实际上仍然是一台主机内部的通信。
只有把服务器代码部署到一个云服务器上,然后通过客户端程序来访问云服务器,此时就是两台主机来通信了。
想要部署程序,需要把程序打个jar包,把jar拷贝过去就行。
总结
DatagramSocket:对socket文件进行了封装
构造方法分为
1)无参:客户端使用,此时端口号由系统分配
2)有参(传入端口号):服务器使用,此时端口号由用户指定
DatagramSocket有两个核心方法:
receive:可能会阻塞
读取一个UDP数据,并且放到DatagramSocket中去
send:
发送一个UDP数据
DatagramPacket对一个UDP数据报进行了封装
它的构造方法是
1)传入空的缓冲区,构造一个空的packet(receive的时候使用)
2)传入一个有数据的缓冲区,指定一下目标的ip和端口,在send的时候使用
3)传入一个有数据的缓冲区,制定一下目标的ip和端口(通过InetSocketAddress这个类来得到),在send的时候使用。