javaEE初阶 网络编程(socket初识)
目录
一.套接字(socket)
1.前提知识:
(1).IP与端口号
(2).TCP/IP五层协议:
2.概念:
(1).简单认识
(2).socket对象
3.传输层两种重要协议
1>:TCP协议 与 UDP协议
2>.特点
(1).连接:通信双方分别存储了对方的关键信息(IP与端口号),可以建立起通信
(2).可靠传输 与 非可靠传输
(3).面向字节流 与面向数据报
(4).全双工
4.UDP套接字编程
1>:介绍:
2>:方法介绍
3>:回显服务器(echo-server)
(1).代码演示
(2).演示:
5.TCP套接字编程
1>:介绍
2>:方法介绍
3>:回显服务器
(1).代码
(2)运行截图
(3).问题:
a.缓冲区问题
b.内存溢出问题
c.一个服务器对多个客户端
6.发送请求的注意
二.总结
1.我们可以操控socket对象 进而控制网卡进行网络传输
2.对于TCP而言
3.对于UDP而言
一.套接字(socket)
1.前提知识:
(1).IP与端口号
IP地址:⽤于定位主机的⽹络地址,简单来说,就是看应用运行在哪台电脑上
端口号:标识服务器对应进程的位置,简单来说,就是看应用在电脑的哪个位置
(2).TCP/IP五层协议:
2.概念:
(1).简单认识
套接字是一组专门用于网络编程的一组类和方法
应用层传送消息到传输层,需要调用对应的函数,而这些函数由操作系统提供且由c实现,把这些方法称为套接字(socket),为了方便使用,Java将其封装为一组类和对象
(2).socket对象
硬盘用于存储数据,而为了方便观察到硬盘使用情况,采用文件的大小来表示硬盘的使用
而网卡用于传输数据,为了方便控制传输内容,我们可以用socket对象来控制网卡来进行数据传输
3.传输层两种重要协议
注:这里只是简单了解,方便后续编程,具体会在新章节讲到
1>:TCP协议 与 UDP协议
TCP与UCP为了网络编程,提供了两组套接字,也就是接下来学习的目标,但在此之前先了解协议特点
2>.特点
TCP: 有连接,可靠传输,面向字节流,全双工
UDP:无需连接,不可靠传输,面向数据报,全双工
(1).连接:通信双方分别存储了对方的关键信息(IP与端口号),可以建立起通信
比如:两个认识的人打电话,如何确定对方身份呢?
TCP的做法(有连接):双方手机互相存储对应的手机号与名字,拨打时看来电显示就可以确定身份,接着接通电话建立连接;删除连接,通信双方互相删除对应的关键信息,手机上则是按下挂断进行连接的释放
UDP做法(无连接):直接拿手机号给你发短信问你是不是那个人,至于对方回复也好,不回复也无所谓,不关心
(2).可靠传输 与 非可靠传输
首先要知道信息在传输时可能会丢失数据,也就是丢包
TCP在传输时会尽可能的传输数据,防止出现丢包现象,但速度稍微慢点,属于可靠传输
UDP在传输时,会以尽可能快把数据传递,坏处就是出现丢包现象,属于不可靠传输
(3).面向字节流 与面向数据报
面向字节流:指数据像水流一样连续传递
面向数据报:明确要传递的内容信息,对方的IP地址与端口号,将这些内容打包为数据报进行传递
(4).全双工
全双工就是支持双方一起通信,不过协议不同会导致细微差距
以打电话为例:TCP全双工就是通信双方同一时间可以同时进行说话或听对方讲话
以发短信为例:UDP全双工就是同一时间,只能有一个人进行消息发送
4.UDP套接字编程
1>:介绍:
DatagramSocket:是UDP类型的socket对象,⽤于发送和接收UDP数据报
DatagramPacket:是UDP数据传输的基本单位
2>:方法介绍
因为方法不好单独演示,所以写一个程序来说明
3>:回显服务器(echo-server)
回显服务器相当于网络编程中的hello word,需要写客户端与服务器,其功能是客户端向服务器发送请求,并打印服务器传回来的响应
请求:客户端向服务器发送的数据
响应:服务器向客户端发送的数据
(1).代码演示
服务器代码:
//服务器
public class EchoServer {// 创建socket对象方便传递读取数据private DatagramSocket socket ;// 服务器运行在本机上,IP地址也在本机,且由操作系统处理// 所以创建服务器时指定服务器的端口号(0-65535)// 一般不考虑 0-1023,因为是操作系统留给其他重要服务的端口号// 除此之外随便指定,若雨其他进程冲突,则会报错,此时再修改即可public EchoServer(int port) throws SocketException {// 初始化socket对象,并指定端口号 portsocket = new DatagramSocket(port);}/*** 对于服务器,核心模版为:* 1.读取请求并解析* 2.根据请求计算响应* 3.把响应传递给客户端*/public void start() throws IOException {System.out.println("server begin");// 因为不清楚客户端何时发消息,为了确保响应及时,采用 while 循环及时读取信息while(true){// 1.读取请求并解析// 创建 DatagramPacket对象存储从socket读取的数据DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);// 从socket读取取客户端的请求到 datagramPacket 里socket.receive(requestPacket);// 此时 datagramPacket 存储的为字节数组,而客户端与服务器最好用字符串// 将字节数组从 0 线标开始将1024个元素转为字符串String request = new String(requestPacket.getData(),0, requestPacket.getLength());// 2.根据请求计算响应String response = process(request);// 3.把响应传递给客户端// 把响应传递给客户端,需要知道: 传递内容 目标IP地址与端口号// 所以构造 DatagramPacket 数据报对象存储这些// 数据报对象包括:字节数组 数组长度 客户端IP与端口号DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);// 4.打印一下日志// 客户端IP 端口号 请求 响应System.out.printf("[%s:%d], request: %s response: %s \n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}/*** 将请求通过 process 方法计算响应* 不过此时,进行最简单的处理*/private String process(String request) {return request;}public static void main(String[] args) throws IOException {EchoServer echoServer = new EchoServer(9909);echoServer.start();}
}
客户端代码:
// 客户端
public class Echo_client {// 创建socket对象方便传递读取数据private DatagramSocket socket;// 存储服务器的 IP 与 端口号private String serverIp;private int serverPort;// 首先客户端要向服务器发送信息,需要存储对应的 IP 与 端口号// 我们希期望初始化客户端时就清楚这些,所以构造方法传递对应参数并存储// 其次,服务器初始化时给定端口号,由程序员操作,所以端口号不会发生冲突// 对于客户端来说不需要指定端口号,因为无法保证指定的与用户的某个程序的端口号相同从而发生冲突public Echo_client(String serverIp,int serverPort) throws SocketException {// 不指定不代表不需要,采用无参构造方法随机指定一个空闲的端口号socket = new DatagramSocket();// 保存服务器IP与端口号this.serverIp = serverIp;this.serverPort = serverPort;}// 此时五元组已确定,即:协议:UDP协议 源IP:本机 源端口号:随机指定的 目标IP:serverIp 目标端口号:serverPort// 可以开始传输数据/*** 客户端需要做从控制台把字符串发送给服务器,从中读取响应* 核心模版为:* 1.读取字符串* 2.构造UDP数据报,发送请求* 3.读取响应*/public void start() throws IOException {Scanner scanner = new Scanner(System.in);// 方便多次发送请求while(true){// 1.读取字符串System.out.print("请输入请求内容>:");String request = scanner.next();if(request.equals("exit")){System.out.println("程序结束");break;}// 2.构造UDP数据报,发送请求// InetAddress.getByName() 获得目标IPDatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(this.serverIp),this.serverPort);socket.send(requestPacket);// 3.读取响应DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);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 {// 给定服务器的IP与端口号Echo_client echo_client = new Echo_client("127.0.0.1",9909);echo_client.start();}
}
(2).演示:
4>:简单翻译服务器
因为服务器的模版都是一样,所以只需继承回显服务器在扩招其他功能即可
public class TranEchoServer extends EchoServer {// 存储 英文与翻译键值对private static Map<String,String> translate = new HashMap<String,String>();;static {translate.put("香蕉","banana");translate.put("苹果","apple");translate.put("你好","hello");translate.put("世界","word");}// 这里不需要给当前翻译服务器端口号,因为它本质是调用回显服务器进行数据传输public TranEchoServer(int port) throws SocketException {super(port);}// 这里 读取响应等操作都是一样,所以只需要重写 "请求计算响应" 这个步骤@Overridepublic String process(String request){return translate.getOrDefault(request,"没有该值翻译");}public static void main(String[] args) throws IOException {TranEchoServer tranEchoServer = new TranEchoServer(10000);tranEchoServer.start();}
}
5.TCP套接字编程
TCP涉及到两种网络模型:
短连接:一个连接,只发送一个响应就关闭
长连接:一个连接,进行多次发送响应后才关闭
1>:介绍
ServerSocket(服务套接字):专门根据客户端的访问返回一个socket对象Socket:负责数据传输与接受
2>:方法介绍
3>:回显服务器
(1).代码
服务器代码:
public class TCPEcho_server {// 创建 serversocket 对象,用于与客户端建立连接返回socket对象private static ServerSocket serverSocket ;// 给定服务器端口号public TCPEcho_server(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("server begin");while(true){// 首先先与客户端进行连接,才能进行后续操作// 此时 socket对象草扩双方的IP与端口号Socket socket = serverSocket.accept();// 接着进行后续连接操作processconnection(socket);}}private static void processconnection(Socket socket) {// 1.服务器要能哪个客户端进行访问,打印日志System.out.printf("[%s : %d]客户端上线\n",socket.getInetAddress().toString(),socket.getPort());// 2.获得流对象进行数据交换 读取客户端的请求与发送响应try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){
// //此处读取麻烦了,选择采用更加优秀的做法
// while(true){
// byte[] bytes = new byte[1024];
// int n = inputStream.read(bytes);
// if(n == -1)break;
// }Scanner scanner = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);// 处理多个请求while(true){// 3.读取请求// 此处采取短连接,当服务器没有读到值,默认本次链接结束if(!scanner.hasNext()){// 代表读取完了System.out.printf("[%s : %d]客户端关闭请求\n",socket.getInetAddress().toString(),socket.getPort());break;}String request = scanner.next();// 4.根据请求计算响应String response = process(request);// 5.发送响应printWriter.println(response);// 6.打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),request, response);}}catch (IOException e){e.printStackTrace();}}private static String process(String request) {return request;}public static void main(String[] args) throws IOException {TCPEcho_server tcpEcho_server = new TCPEcho_server(12345);tcpEcho_server.start();}
}
客户端代码:
public class TCPEcho_client {// 创建 socket 对象用于进行数据交流public Socket socket = null;// 当 socket 对象赋值时会根据所传输的服务器IP与端口号自动创建连接// 给定所连接的服务器的IP与端口号public TCPEcho_client(String IP,int port) throws IOException {socket = new Socket(IP,port);}public void start(){Scanner scanner = new Scanner(System.in);// 创建流对象进行数据传输try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){Scanner scannerNewWork = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while(true){// 1.从控制台读取数据System.out.printf("输入数据>:");String request = scanner.next();// 2.发送请求writer.println(request);// 3.读取响应if(!scannerNewWork.hasNext()){break;}String response = scannerNewWork.next();// 4.把响应展示在控制台上System.out.println(response);}}catch(IOException e){e.printStackTrace();}}public static void main(String[] args) throws IOException {TCPEcho_client tcpEcho_client = new TCPEcho_client("127.0.0.1",12345);tcpEcho_client.start();}
}
(2)运行截图
(3).问题:
a.缓冲区问题
对于TCP协议而言,内置了一个缓冲区,当我们进行写入操作时,会先把数据放入缓冲区中,等满了才会读取,为了保证数据的快速读取,提供了flush()冲缓冲区方法,会把当前缓冲区数据冲出使其被可读取
对于UDP而言,也有缓冲区,不过在内核里面,应从程序用不到
此时在代码中加入
此时代码就正确了
b.内存溢出问题
对于socket来说,也是个文件,使用时要消耗资源,而服务器的serversocket与客户端的socket的生命周期随进程结束而结束,这个不用管,但注意:在服务器accept返回的socket对象还没关闭呢
此时只要客户端退出,直接close()关闭socket对象与连接
c.一个服务器对多个客户端
当我们创建多个客户端
可以发现,只有客户端1建立连接了,客户端二不管进行怎么操作在服务器都没显示,这是不正确的
原因: 创建的客户端在构造方法处就会与服务器建立联系
当客户端1建立联系后,就会与服务器进行数据传输,此时服务器在等待客户端1输入数据,而其他客户端因为服务器在忙碌此时无法建立联系,自然就会在accept()处阻塞,等待服务器空闲时再建立联系
解决方法:
1.加入多线程,让每个线程负责一个客户端
public void start() throws IOException {System.out.println("server begin");while(true){Socket socket = serverSocket.accept();// 每次建立连接都的socket,都会分配线程执行当前连接Thread thread = new Thread(new Runnable() {@Overridepublic void run() {processconnection(socket);}});thread.start();}}
每个线程发送"我是客户端X",可以清晰看到都可以与服务器进行连接
此时结构图为
2.引入线程池: 当客户端多了,线程的创建与销毁都伴随着大量的资源消耗,若是可以提前把线程创建好,需要时直接来取,可以明显减少资源消耗
public void start() throws IOException {System.out.println("server begin");// 不能使用 FixedThreadPool,线程数目固定 ExecutorService executorService = Executors.newCachedThreadPool();while(true){Socket socket = serverSocket.accept();executorService.submit(()->processconnection(socket));}}
3.文件IO多路复用:一个线程负责多个客户端
对于线程池来说,线程数量也是有上线的,所以线程池一般负责短连接,但进入长连接后,每个线程长时间运行,很快就会达到最大线程数量,所以引入文件IO多路复用,让一个线程负责多个客户端,极大缓解这种情况
6.发送请求的注意
对这个代码,采用next() 读取请求,逻辑为:遇到空白符停止读取并返回,那么发送时要使用如图的方法,自动加\n,否则就对方无法正常读取
二.总结
1.我们可以操控socket对象 进而控制网卡进行网络传输
2.对于TCP而言
(1).需要通过accept返回连接后的socket对象,通过该对象调用InputStreasm OutputStream流对象进行数据传输
(2).传输数据其实传输到缓冲区,要用flush()进行冲刷操作读取数据
(3).对于accept返回的socket对象要及时close()释放,防止文件描述表满了无法打开文件
(4).一个服务器对多个客户端需要引入多线程
(5).发送请求或响应注意用 println(),方便后续读取
3.对于UDP而言
通过数据报进行数据传输,需要注意数据报传输时要保存对方的IP与端口号
这里只是简单的编程学习,为后续网络编程打基础,如果对你有帮助,点一个吧~~~