我们网络编程的核心: Socket API,操作系统为我们应用程序提供的API,我们的Socket是和传输层密切相关的。Socket API 套接字,翻译是插座
传输层为我们提供了两个最核心的协议UDP/TCP,因此Socket API也为我们提供了两种风格TCP/UDP。(有第三种unix早就没人用了)
简单认识一下TCP/UDP:
TCP 有连接 可靠传输 面向字节流 全双工
UDP 无连接 不可靠传输 面向数据报 全双工
有链接/无连接:
打电话就是有连接的,需要连接建立了才能通信,连接建立需要双方来接受
发微信短信就是无连接的,直接发消息,不管对方要不要连接
可靠/不可靠传输:
发送方可以知道自己的消息是不是发过去了,还是丢了。
打电话是可靠传输
发微信短信就是不可靠传输,带已读功能的可以视为可靠传输,像QQ,wx这种没有已读功能的就可以视为不可靠传输
注意:可不可靠和有没有连接没有任何关系
面向字节流:数据传输就和文件读写类似,“流式”的
面向数据报:数据传输则以一个个的“数据报”为单位(一个数据报可能是若干个字节,带有一定的格式的)
全双工:一个通信通道,可以双向传输,也就是说,既可以发送,也可以接收
TCP特点:
使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
传输前,采用”三次握手"方式建立连接,所以是可靠的
在连接中可进行大数据量的传输
连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率低应用场景:对信息安全要求较高的场景,例如:文件下载、金融等数据通信
UDP特点:
UDP是一种无连接,不可靠传输协议
将源IP、目的IP和端口封装成数据包,不需要建立连接
每个数据包大小限制在64kb内
发送不管对方是否准备好,接收方收到也不确认,所以是不可靠的
可以广播发送,发送数据结束时无需释放资源,开销小,速度快
应用场景: 语音通话,视频会话等
DatagramSocket 这个类表示一个 Socket 对象,操作系统操作网卡,不是直接操作的,而是把网卡抽象成了特殊的文件,称为 socket 文件,相当于是文件描述符表上的一项。
普通的文件对应的硬件设备是硬盘,socket文件对应的硬件设备是网卡。
一个 Socket 对象就可以与另一台主机进行通信了,如果要和多个不同的主机通信,就需要创建多个 Socket 对象
receive 方法参数传入的是一个空的 DatagramPacket 对象,receive 方法内部会对这个对象进行填充,从而构造出结果数据,我们称这样的参数为输出型参数。
本质上,不是进程和端口建立联系,而是进程中的socket对象和端口建立了联系
DatagramPacket 是 UDP socket 进行发送和接收的数据报,表示 UDP 中传输的一个报文,构造这个对象,可以指定一些具体的字节数组进去,作为持有数据的缓冲区。
DatagramPacket的一些方法:
普通服务器做的工作:收到请求,根据请求计算响应,返回响应,最重要的环节就是计算响应这一部分。
回显服务器(echo server)省略了最重要的环节 "计算响应“ 这一部分,接收到什么就返回什么。代码没有实际的业务,没有太大作用和意义,只是展示socket api的基本用法。
作为一个真正的服务器,一定“根据请求计算响应”这个环节是最最最重要的。
对于服务器来说,创建socket对象的同时,要让他绑定上一个具体的端口号
第一步:创建一个socket对象
我们网络编程,本质上是要操作网卡。
但是网卡不方便我们直接操作,于是我们操作系统内核中,使用“socket"这样的文件来抽象表示网卡,所以我们要想进行网络通信,首先得有一个socket对象。
public class EchoServer {
private DatagramSocket socket = null;
public UdpEchoServer() throws SocketException {
socket = new DatagramSocket();
}
}
第二步:为服务器绑定一个 具体的端口号
我们的服务器,在真正创建对象的时候,需要绑定一个具体的端口号,为啥是具体的呢?
服务器是网络传输中,是属于比较被动的一方,如果让操作系统随机分配端口,那么客户端就不知道服务器端口是哪个了 ,也就无法进行通信了。
如果把 服务器的端口号 比作 食堂商家的窗口号 ,作为食堂商家就需要一个具体的窗口号为食客服务,在计算机中也是如此,服务器必须要有一个具体的端口号为客户端进行服务。
public EchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
第三步: 启动客户端
读取客户端发来的请求是啥
由于服务器不只是为一个客户端提供服务就完了,需要服务很多的客户端
为什么 while(true)死循环:因为服务器需要 7*24 小时运行
public void start() throws IOException {
System.out.println("客户端启动!");
while(true){
//只要有客户端过来就可以提供服务
//1、读取客户端发来的请求是啥
// receive 方法的参数是一个输出型参数,需要先构造好一个空白的DatagramPacket 对象,交给receive来进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096] ,4096);
socket.receive(requestPacket);
}
}
DatagramSocket 这个类的receive能阻塞,是因为操作系统原生提供的API(recv)就是阻塞的函数,这里的阻塞不是Java实现的,而是操作系统内核里实现的。
第四步:构造字符串
我们的UDP传输的基本单位是 DatagramPacket.
此时我们服务器接收到的 DatagramPacket 是一个特殊的对象,并不方便我们直接进行处理,我们可以把这里包含的数据拿出来,构造成一个字符串。
String request = new String(requestPcket.getData(),0, requestPcket.getLength());
我们在创建DatagramPacket给的最大长度是4096,但我们实际可能只用了一小部分,因此我们在构造字符串时,通过getLength()获取数据报实际的长度,只把实际有效的部分构造成字符串即可。
第五步:处理请求—— process方法
我们获取到了客户端的请求之后然后我们对请求进行处理(根据请求计算响应),我们这里是 回显服务器,响应和请求相同,实现的是接收什么,回应什么。
process方法表示“根据请求计算响应”,这里是回显服务器,响应和请求相同,实现的是接收request,就回应request。
public String process(String request) {
return request;
}
第六步: 响应请求—— send方法
然后我们将这个响应发送给客户端,需要用到 send 方法,send 的参数也是 DatagramPacket ,那么在send前,我们就需要先构造出 DatagramPacket 对象。
此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应回来的数据来构造。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length);
因为我们的DatagramPacket不认字符只认字节,所以我们将response转换为字节数组。
response.getBytes()拿到字节数组,response.getBytes().length获取的是字节的个数。
答案是不可以,因为response.length()获取的是字符的个数,response.getBytes().length获取的是字节的个数。
我们这里的DatagramPacket的构造还是有一点点问题,我们这里的数据是构建好了,那给谁发呢?
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPcket.getSocketAddress());
我们在参数中,应该传入客户端的地址信息。
然后进行发送 ->
socket.send(responsePacket);
数据到达网卡,经过内核层层分用,最终到达了UDP传输层协议,调用 receive 相当于是执行内核中udp相关的代码,将UDP数据报的载荷取出来,拷贝到用户提供的byte[] 数组中。
//UDP 版本的回显服务器
public class UdpEchoServer {
//网络编程,本质上是在操作网卡
//但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的“socket”文件来抽象表示网卡
//因此,进行网络通信,势必需要先有一个socket对象
private DatagramSocket socket = null;
//对服务器来说,创建socket对象的同时,要让他绑定上一个具体的端口号
//服务器一定要关联上一个具体的端口
//服务器是网络传输中,被动的一方,如果是操作系统随机分配的端口,客户端会找不到,也就无法进行通信
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器不是只给一个客户服务,需要服务 多个客户
while(true){
//只要有客户端过来就可以提供服务
//1、读取客户端发来的请求是啥
//receive 方法的参数是一个输出型参数,需要先构造好一个空白的DatagramPacket 对象,交给receive来进行填充
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、把响应写回到客户端,send 的参数也是DatagramPacket,需要把这个Packet 对象构造好
//此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造
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(),request,response);
}
}
//根据请求,返回响应
//这里是回显服务器,就直接返回了
public String process(String request ){
return request;
}
public static void main(String[] args) throws IOException {
//端口号可以在 1024-65535 这个范围内 随便指定
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
我们一次网络通信涉及到五元组:
源IP,源端口,目的IP,目的端口,协议类型。
构造客户端Socket对象时,不需要显式的去绑定一个端口,而是由系统分配一个空闲端口。
服务器的端口:需要固定指定,为了方便客户端找到服务器程序。
客户端的端口:由系统自动分配的,如果我们手动指定,可能会与客户端其他程序的端口冲突
为什么服务器不怕冲突?
因为服务器上面的程序可控,而客户端是运行在我们用户电脑上,环境复杂,更不可控。
第一步:创建Socket对象
首先我们需要创建一个Socket对象,并且获取配置服务器的ip和端口号
private DatagramSocket socket = null;
private String serverIp = null;//服务器的ip
private int serverPort = 0;//服务器端口号
public EchoClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
第二步:从控制台接收我们需要发送端数据。
System.out.print("> ");
String request = scan.next();
if(request.equals("exit")) {
System.out.println("客户端退出");
break;
}
第三步:构造字符串
我们将request字符串构造成DatagramPacket进行发送。
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
我们在构造DatagramPacket的时候,需要将ip和端口号都传入,此处需要传入的IP是32位的整数形式,但我们这里的ip是字符串,所以需要使用InetAddress.getByName进行转换,然后进行发送。
socket.send(requestPacket);
第四步:接收服务器响应
我们将请求发送给服务器之后,我们来接收一下服务器的响应。
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
客户端完整代码:
//UDP 版本的回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
//一次通信,需要有两个ip ,两个端口
//客户端的IP 是已知的 127.0.0.1
//客户端的port是系统自动分配的
//服务器IP和端口 也需要告诉客户端,才能顺利把消息发给服务器
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 scan = new Scanner(System.in);
while (true) {
//1、从控制台读取要发送的数据
System.out.print("> ");
String request = scan.next();
if(request.equals("exit")) {
System.out.println("客户端退出");
break;
}
//2、构造成UDP请求,并发送
//构造这个 Packet 的时候,需要把serverIp和port 都传入过来,但是此处IP地址需要填写的是一个32位的整数形式
//上述的IP地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3、读取服务器的UDP响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
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();
}
}
客户端服务器程序,一个服务器是给许多客户端提供服务的,但是我们IDEA默认只能启动一个客户端,我们需要手动设置一下。
现在我们就可以创建多客户端与服务器进行通信了。
这里的报错是 端口冲突
也就是说 一个端口只能被一个进程使用,如果有多个使用就不行。