赞
踩
目录
3.基于UDP Socket的客户端服务器程序(回显服务器echo server)
数据报,Datagram,通过网络传输的数据的基本单元,包含一个报头(header)和数据本身,其中报头描述了数据的目的地以及和其它数据之间的关系。
概念:socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
TCP/IP五层网络模型:应用层,传输层,网络层,数据链路层,物理层。其中应用层主要是应用程序,传输层和网络层是是由系统内核封装的,数据链路层和物理层主要是由硬件和驱动实现的。在网络分层下,数据的传输离不开封装和分用。
程序员写网络程序 ,主要编写的是应用层代码,其他下面四层是程序员无法改变的。当应用程序需要将数据上传,此时就需要上层协议,调用下层协议,应用层调用传输层,传输层给应用层提供一组api,这组api就是套接字socket。
系统主要给我们提供两组Socket api:
1.基于UDP的api ;
2.基于TCP的api。
UDP协议和TCP协议的特点
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
有/无连接:使用UDP/TCP通信的双方,各自是否需要刻意保存对端的相关信息;
可靠传输:信息发出去,尽可能的传输过去。
不可靠传输:信息发出去,不关注结果,不关注是否传输过去。
面向数据报:以一个UDP数据报为传输的基本单位。
面向字节流:以字节流为传输的基本单位,读写方式十分灵活。
全双工/半双工:一条路径双/单向通信。
主要提供了两个类:DatagramSocket(Socket对象),DatagramPacket(udp数据报)
关于“报”,是网络传输数据的基本单位,这些基本单位主要包括:报(datagram)(udp中使用),包(packet)(ip中使用),段(segment)(tcp中使用),帧(frame)(数据链路层中使用)。日常生活中,不会特意区分这些单位,但是写研究论文需要区分。
Socket对象:相当于对应到系统中的一个特殊文件(socket文件),这个文件并非对应到硬盘上的某个数据存储区域,而是对应到网卡这个硬件设备。进行网络通信,离不开socket文件这样的对象,借助socket文件对象,才能间接的操作网卡(相当于遥控器)。
向socket对象中写数据,相当于通过网卡发送消息;向socket对象中读数据,就相当于通过网卡接收消息。
下图片的以太网适配器就是一个有线网卡:
无线网卡:
没有网卡就不能上网,一般是集成在主板上的。
文件:广义上,代指很多计算机中的软件/硬件资源;狭义上,代指硬盘上的一块数据存储区域。
DatagramSocket是UDP Socket用于发送和接收UDP数据报。
DatagramSocket构造方法:绑定一个端口号(服务器),也可以不显示指定客户端
方法签名 | 方法说明 |
DatagramSocket | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket(int port):此处Socket对象可能被客户端/服务器使用,服务器的socket往往需要关联一个具体的端口号(不变);客户端这里不需要手动指定,系统自动分配即可(可以改变)。
DatagramSocket方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字,与文件操作一样,用完一定要关闭,否则会出现文件泄露问题 |
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket,不需要设置地址进去,主要以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket,显示地址设置,通常以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length),address指定目的主机的IP和端口号 |
DatagramPacket方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
作用:客户端发送一个请求,服务器返回一个一模一样的响应。没有什么实际作用,只保留了最核心的发送接收环节。
服务器的三个核心工作:
读取请求并解析
根据请求计算响应
把相应返回到客户端
整个回显服务器执行流程:
客户端读取用户输入
客户端构造请求,并发送
服务器读取用户请求数据
服务器根据请求计算响应
服务器把响应写回到客户端
客户端读取服务器的响应
客户端把响应转成字符串,并显示出来
以下代码执行顺序:
服务器先启动,执行到receive进行阻塞
客户端运行之后,从控制台读取数据,并进行send
此时客户端和服务器同时进行。客户端这边,send以后,继续往下执行,在receive里读取响应,读取以后会阻塞等待;服务器这边,就从receive返回,读取到请求数据(从客户端来的),往下走到process生成响应,然后再往下走到send并打印日志
客户端这边,当收到send回到的数据以后,就会接触阻塞,进行打印操作;服务器这边进入下一顿循环,再次阻塞到receive这里
客户端继续进行下一轮循环,阻塞在scanner.next这里等待用户执行新的内容
具体代码如下:
(1)服务器程序
- package network;
-
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
-
- public class UdpEchoServer {
- //定义一个socket对象
- //通过网络通信,必须要使用socket对象
- private DatagramSocket socket = null;//定义一个socket对象
- //绑定一个端口,不一定能成功
- //某个端口已经被别的进程占用了,此时绑定操作就会出错
- //同一个主机上,一个端口,同一时刻,只能被一个进程绑定
- public UdpEchoServer(int port) throws SocketException {
- //构造socket时,指定要关联/绑定的端口
- socket = new DatagramSocket(port);
- }
-
- //启动服务器的侏罗纪
- public void start() throws IOException {
- System.out.println("服务器启动");
- while(true) {//时刻向客户端提供服务
- //每次循环,有三个步骤:
- //1.读取请求并解析
- //构造空对象
- DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
- //从网卡中读取数据
- //一旦服务器start,就会立即执行receive,如果此时客户端没有发送数据,此时的receive就是阻塞状态
- socket.receive(requestPacket);
- //将数据包(二进制)转换成string,处理此请求
- String request = new String(requestPacket.getData(),0,requestPacket.getLength());
- //2.根据请求计算响应(省略)
- String response = process(request);
- //3.把响应结果写回到客户端
- //根据response字符串。构造一个DatagramPacket
- //与请求packet不同,此处构造
- DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
- //requestPacket是从客户端收来的,getSocketAddress就会得到客户端的ip和端口
- ,requestPacket.getSocketAddress());
- //客户端send请求,服务器receive请求
- //服务器send响应,客户端receive响应
- socket.send(responsePacket);
- System.out.printf("[%s:%D]req:%s,resp:%s\n",responsePacket.getAddress().toString(),
- requestPacket.getPort(),request,response);
-
- }
- }
-
- //此方法作用是根据请求计算响应
- //回显程序中没有写具体的请求和回应
- //如果写别的程序就可以修改process方法,根据需求重新构造响应
- public String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
- udpEchoServer.start();
-
- }
- }
(2)客户端程序
- 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 {
- Scanner scanner = new Scanner(System.in);
- while (true) {
- //1.先从控制台读取一个字符串
- //先打印一个提示符,提示用户要输入内容
- System.out.println("->");
- String request = scanner.next();
- //2.把字符串构造成UDP packet,并进行发送
- //这个构造,就是把数据构造成DatagramPacket,一方面需要String中的getBytes数组,另一方面,需要指定服务器的ip和端口
- //此处不是通过inetAddress直接构造的,而是分开设置的,一方面设置字符串的ip,一方面设置端口号
- 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显示出来
- String response = new String(responsePacket.getData(),0,requestPacket.getLength());
- System.out.printf("%req:%s,resp:%s\n",request,response);
- }
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
- udpEchoClient.start();
- }
- }
对于UdpEchoServer来说,socket对象出循环就结束了生命周期,循环结束,意味着start结束,意味着main方法结束,即进程结束,当进程技结束以后,所有的文件资源就自动释放了,所以上述代码就不必显示调用close方法。
- package network;
-
- import java.io.IOException;
- import java.net.SocketException;
- import java.util.HashMap;
- import java.util.Map;
-
- //使用继承,复用前面写过的代码
- public class UdpDictServer extends UdpEchoServer{
- //使用一个hash表,存储单词
- //翻译的本质就是查表
- private Map<String,String> dict = new HashMap<>();
-
- public UdpDictServer(int port) throws SocketException {
- super(port);
-
- //向表中添加元素
- dict.put("dog","小狗");
- dict.put("cat","小猫");
- dict.put("fuck","卧槽");
-
- }
-
- //根据请求计算响应
-
- @Override
- public String process(String request) {
- return dict.getOrDefault(request,"单词没有查到");
- }
-
- public static void main(String[] args) throws IOException {
- UdpDictServer udpDictServer = new UdpDictServer(9090);
- udpDictServer.start();
- }
- }
ServerSocket构造方法:构造是指定一个具体端口,让服务器绑定该端口。
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket方法:
accept()方法就是接受,服务器是被动接受的一方,客户端是主动接受的一方
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
Void close() | 关闭此套接字 |
Socket 构造方法:服务器IP和端口号,在客户端new Socket对象的时候,就会尝试和指定的ip端口的目标建立连接
方法签名 | 方法说明 |
Socket(String host, intport) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket方法:面向字节流,通过Socket对象拿到字节流对象,就可以通过字节流对象进行数据传输了,从InputStream里面读取数据,就相当于从网卡接收;在OutPutStream里面写数据,就相当于从网卡发送。
方法签名 | 方法说明 | |
InetAddress getInetAddress() | 返回套接字所连接的地址 | |
InputStream getInputStream() | 返回此套接字的输入流 | |
OutputStream getOutputStream() | 返回此套接字的输出流 |
长连接(long connnection),指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
短连接(short connnection),是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时才去建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送。
基于TCP套接字的回显程序执行顺序:结合代码观看
1.TcpEchoServer线启动,运行start(),阻塞状态
2.TcpEchoClient启动,会调用socket的构造方法,和服务器进行连接,连接成功之后,accept就会返回
3.对于服务器,进入processConnection方法,尝试从客户端读取请求,由于此时客户端没有发送请求,此时读取操作处于阻塞状态;对于客户端,从控制台读取用户输入。
4.当用户输入后,客户端就会真正发送请求,同时往下执行到,读取服务器响应,再次阻塞。
5.服务器收到客户端的请求之后,从next返回,执行process,执行println将响应写回给客户端。
6.服务器重新回到循环开头位置,继续尝试读取请求,并且阻塞;客户端收到服务器的响应,就可以把结果显示出来了,同时进行下次循环,等待用户输入。
(1)TcpEchoServer
- package network;
-
- 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 {
- //serverSocket相当于外场的拉客中介,只能有一个
- private ServerSocket serverSocket = null;
-
- public TcpEchoServer(int port) throws IOException {
- serverSocket = new ServerSocket(port);
- }
-
- public void start() throws IOException {
- //使用线程池,使得一个连接的所有请求处理完,该线程不会直接被销毁,而是换到池子里面,下次连接时还能直接使用
- ExecutorService executorService = Executors.newCachedThreadPool();
- System.out.println("服务器启动");
- while (true) {
- //clientSocket相当于内场带每个用户讲解房子的中介,可以有多个
- Socket clientSocket = serverSocket.accept();
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
- //通过processConnection此方法来处理一个连接
- //读取请求
- //根据请求计算响应
- //将相应返回到客户端
- private void processConnection(Socket clientSocket) throws IOException {
- System.out.printf("[%s:%d]客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
- //try()中,允许写多个流对象,多个对象中使用分号来分割
- try(InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()){
- //相当于字符流
- //使用scanner的方式读取数据
- Scanner scanner = new Scanner(inputStream);
- //将outputStream转换为字符流
- PrintWriter printWriter = new PrintWriter(outputStream);
- while(true) {
- //1.读取请求
- //每个请求是个字符串(文本数据)
- //请求与请求之间,使用\n来分割
- //hasNext():判断接下来是否还有数据
- //如果客户端关闭连接,hasNext就会返回为false,循环就会结束
- if (!scanner.hasNext()) {
- //读取到流结尾(也就是客户端关闭了)
- //输出客户端ip地址,输出客户端端口号
- System.out.printf("[%s:%d]客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
- break;
- }
- //如果客户端有数据,hasNext就会返回为true,进一步使用下面的next方法来读取出这一段字符串的内容
- //直接使用scanner读取一段字符串
- //next往后一直都,督导空白符结束(空格,换行,制表符,翻页符等等)
- String request = scanner.next();
- //2.根据请求计算响应
- String response = process(request);
- //3.把响应写回给客户端,next读操作结果是不带换行的,响应也需要带上换行
- printWriter.println(response);
- printWriter.flush();
- System.out.printf("[%s:%d]req:%s;resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(), request, response);
- //回显服务器,响应和请求一模一样
- }
- } catch(IOException e){
- e.printStackTrace();
- } finally {
- //clientSocket可以有多个,而且生命周期端,所以需要关闭
- clientSocket.close();
- }
- }
- //根据请求计算响应
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
- tcpEchoServer.start();
- }
- }
(2)TcpEchoClient
- package network;
-
- 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 serveIp,int port) throws IOException {
- //将客户端于服务端建立TCP连接
- //连接上了,服务器的accept就会返回,在此之前,accept是处于阻塞状态的
- socket = new Socket(serveIp,port);
- }
-
- public void start() {
- Scanner scanner = new Scanner(System.in);
- try (InputStream inputStream = socket.getInputStream();
- OutputStream outputStream = socket.getOutputStream()) {
- PrintWriter printWriter = new PrintWriter(outputStream);
- Scanner scannerFromSocket = new Scanner(inputStream);
- while (true) {
- //1.把键盘上读取用户输入的内容
- System.out.print("->");
- String request = scanner.next();
- //2.把读取的内容构造成请求,发送给服务器
- //发送带有换行
- //以下步骤只是把数据写入了内存的缓冲区中,等到缓冲区满了(没有满是因为数据不够多),才会真正写网卡
- printWriter.println(request);
- //使用flush()方法,手动刷新缓冲区,将数据立即写入网卡
- printWriter.flush();
- //3.从服务器读取相应内容
- String response = scannerFromSocket.next();
- //4.把响应结果显示到控制台上
- System.out.printf("req:%s;resp:%s\n", request, response);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
- tcpEchoClient.start();
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。