1.网络编程的基本概念
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。对于我们程序员来说,我们比较关注应用层到传输层这一操作,我们代码的书写是发生在应用层的,然后通过传输层提供的API(UDP和TCP)进行包装,再由应用层发送给传输层。
1.1为什么需要网络编程
这个最为直接的说话莫过于人民需要他了,哪里有需求,哪里就提供响应嘛。
相比于本地的资源,网络上有这更为丰富的资源,而网络上的资源又需要通过网络编程来上传,因此就相互督促相互促进了对方的发展。
1.2服务端与用户端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
简单的说,客户端会接收到客户的请求,再将客户的请求发送给服务端;服务端将请求按照业务逻辑完成响应,再将响应返回给客户端,客户端再将响应按照操作显示或者运作。
1.3网络编程五元组
1.源IP:就是发送方IP.
2.源端口:发送方端口号, 服务器需要手动指定, 客户端让系统随机分配即可.
3.目的IP: 接收方IP, 包含在拿到的数据报中, 服务器响应时的目的IP就在客户端发来的数据报中, 客户端发送请求时的目的IP就是服务器的IP.
4.目的端口: 接收方端口号包含在数据报中, 服务器响应时的目的端口就在客户端发来的数据报中, 客户端发送请求时的目的端口就是服务器的端口号.
5.协议类型:如UDP/TCP.
1.4套接字的概念
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制
套接字的表示方法:Socket=Ip接口:端口号
2.UDP套接字编程
2.1UDP套接字的特点
特点如下:
无连接:不关心和谁对话,也不会刻意留意对方。
不可靠的:不关心他收没收到。
面向数据报:以一个UDP数据报为单位。
全双工通信:一条路径,但是路径两方都可以对话。(双向对话)
2.2UDP套接字API
UDP套接字的API中主要包括两个类 :1.DatagramSocket 2.DatagramPacket。
下面就开始介绍这两个类
2.2.1DatagramSocket类
DatagramSocket类就是实例一个UDP版本的套接字也是就数据包套接字。Socket对象对应到系统中的一个特殊文件(socket文件),socket文件不是数据存储区域的一部分,而是对应到网卡这个硬件设备,为什么对应到网卡呢?因为网卡是一个硬件,对于代码而已,不好直接操作,因此将它抽象成了一个文件进行间接操作。
DatagramSocket类构造方法:
我们一般使用第二个方法,我们知道端口号是在一个计算机中寻找一个程序的“门牌号”。实际操作时,如果端口号是系统随机绑定的话,我们就不知道我们服务端服务于哪一个客户端了,就没有办法找到客户端的家了。
DatagramSocket类方法:
第一个receive方法它的参数需要准备一个空的DatagramPacket对象(对象需要给予存储空间),它把DatagramPacket实例对象再装入到准备好的空的DatagramPacket对象中去 ,为什么这样做呢?就像我们平常接收东西时,我们需要一个载体来承接这个物件,举个例子吧,当我们盛饭时,我们不能够直接用手去捧着饭,而是需要一个空饭盒去接打的饭。
第二个send方法他是DatagramPacket实例对象载入到接收缓冲区。
第三个close方法,因为DatagramSocket类属于文件资源,当我们不用的时候,我们需要将它给手动关闭了。
2.2.2DatagramPacket类
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket类构造方法
这两个构造方法都会使用到,第一个构造方法:他就是我们上方说的空的DatagramPacket对象也就是我们举例说的“空饭盒”,第二个构造方法是在我们将接收到的请求进行封装时使用的,假设我们接收到的请求是字符串,我们就要将它转化成byte数组,再算出长度,并且需要指定它要发送到那里去也就是SockerAddress类的一个子类InetSocketAddress类它里面包含目的ip和目的端口号。
DatagramPacket类方法:
2.2.3基于UDP的回显程序
UDP服务端:
服务器设计逻辑:
1.创建DatagramSocket对象,指定服务器端口号。
2.服务器启动了,我们需要一个DatagramPacket实例运来当作载体,当没有请求时,在这里就会发生阻塞,有请求时,就写入到DatagramPacket实例好的载体中。
3.接收到数据后,我们将数据拆分成我们需要的格式,因为我们是一个回显,故我们只需将它改成字符串就行了,再调用process方法,在这里完成业务需求,我们这里只需返回字符串就像了。
4.将处理好的请求,再封装一下,封装成DatagramPacket实例对象,将这个响应发生使用send方法发送给客户端即可。
package UdpNetWork; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; //服务器端 public class UdpEchoServer { private DatagramSocket socket=null; public UdpEchoServer(int port) throws SocketException { this.socket=new DatagramSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true) { //requestPacket是一个载体,receive方法会将发送过来的数据放入到这个载体中 DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096); socket.receive(requestPacket); //将发来的数据拆解 String request=new String(requestPacket.getData(),0, requestPacket.getLength()); //做出响应 String response=process(request); //将做出的响应包装起来,发给客户端 DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length, requestPacket.getSocketAddress()); socket.send(responsePacket); //打印日志 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 { UdpEchoServer udpEchoServer=new UdpEchoServer(9090); udpEchoServer.start(); } }
UDP客户端:
客户端设计逻辑:
1. 客户端需要知道服务端的位置,因此我们需要服务端的ip地址和端口号,故成员变量包含Ipserver、serverPort以及一个DatagramSocket对象(可以理解为整合ip和端口号)。
2.开始运行客户端,通过Scaner接收客户的请求。
3.拿到请求后,我们需要将它封装成DatagramPacket对象,发送给服务端。
4.发送完就等待服务端返回响应。
5.等到拿到响应之后,我们就可以对响应做出操作了,因为我们这里是回显,我们只需将返回过来的字符串打印出来即可。
package UdpNetWork; 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 int serverPort; private String IpServer; public UdpEchoClient(int port, String ipServer) throws SocketException { socket=new DatagramSocket(); this.serverPort=port; this.IpServer=ipServer; } public void start() throws IOException { Scanner sc=new Scanner(System.in); while (true) { System.out.print("输入字符->"); String request=sc.next(); //将输入的指令打包 DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(IpServer),serverPort); //发送给服务器端 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.printf("req:%s,resp:%s\n",request,response); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient=new UdpEchoClient(9090,"127.0.0.1"); udpEchoClient.start(); } }
2.2.4基于UDP的单词查询
对于这个服务端,我们不需要再写一边start方法了,我们可以直接让它继承udpEchoServer类,并且,我们在上方服务端,我们将对请求处理的部分拿出来当作了一个单独的函数,我们在将上方的函数进行重写就行了,还有就是,我们需要一个存储单词,并且它还方便查询的东西,我们就想到一个数据结构就是Map,刚好可以适合这个系统。
package UdpNetWork; import java.io.IOException; import java.net.SocketException; import java.util.HashMap; import java.util.Map; public class UdpDictServer extends UdpEchoServer{ //字典表 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(); } }
3.TCP套接字编程
3.1TCP套接字的特点
特点如下:
- 1.有链接:双方通信,都需要可以留意对方信息。
- 2.可靠传输:尽可能的将数据传输给对方,但也不能保证100%。
- 3.面向字节流:以一个字节传输为基本单位。
- 4.全双工:双方都可以向对方发送信息。
3.2TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
长连接与短链接的区别:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
3.3TCP套接字的API
TCP套接字的API中主要包含两个类:1.ServerSocket 2.Socket
下面介绍这两个类
3.3.1ServerSocket类
这个类是在看名字就很好理解,它主要用于服务端,它的作用是创建一个服务端套接字。
第一个accept方法它是接收客户端的信息,如果客户端没有请求,那它就会阻塞等待,如果有请求,就会建立连接,它的返回值是一个套接字。
3.3.2Socket类
这个类就是创建一个套接字实例对象,他的成员变量有两个:IP与端口号。
3.3.3基于TCP的回显程序
服务端
服务器设计逻辑:
- 1.创建一个ServerSocket类对象,指定服务器端口号。
- 2.开始运行服务端;通过第一步实例好的对象调用accept方法,如果有无请求,就阻塞,如果有请求,就发到下面进行处理。
- 3.进入处理请求方法,将请求通过读接收缓冲区写入,再将写入的请求发给处理程序进行处理。
- 4.处理完毕后,将响应发给客服端。
package TcpNetWork; 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; 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("服务器启动"); while (true) { //接收到客服端请求 Socket clientSocket=serverSocket.accept(); //将请求发给解决请求的方法 processConnection(clientSocket); } } private void processConnection(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 客户端上线了\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); try (InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()){ // 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n // 不是不能做, 而是代码比较麻烦. // 为了简单, 把字节流包装好了更方便的字符流~~ Scanner sc=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true) { //1.接到客服端请求 if (!sc.hasNext()) { System.out.printf("[%s:%d] 客户端下线了\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request=sc.next(); //2.对请求做出响应 String response=process(request); //3.把响应返回给客户端 printWriter.println(response); printWriter.flush(); System.out.printf("[%s:%d] request:%s response:%s ",clientSocket.getInetAddress().toString(), clientSocket.getPort(),request,response); } } catch (IOException e) { e.printStackTrace(); }finally { clientSocket.close(); } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer=new TcpEchoServer(9999); tcpEchoServer.start(); } }
客户端
客户端设计逻辑:
- 1.创建一个Socket实例,指定服务器的ip和端口号。
- 2.开启客户端,输入请求,并刷新接收缓存区。
- 3.等到服务端给出响应,接收响应,对响应做出动作。
package TcpNetWork; 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(int port, String serverIp) throws IOException { socket=new Socket(serverIp,port); } public void start() { Scanner sc=new Scanner(System.in); try(InputStream inputStream=socket.getInputStream(); OutputStream outputStream=socket.getOutputStream()) { Scanner scannerFromSocket=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true) { //1.从键盘上读取请求 System.out.print("输入指令->"); String request=sc.next(); //2.将请求发送给服务器端,并刷新接收缓存区 printWriter.println(request); printWriter.flush(); //3.接收服务器端的信息 String response=scannerFromSocket.next(); //4.把信息打印到控制台 System.out.printf("request:%s response:%s\n",request,response); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient tcpEchoClient=new TcpEchoClient(9999,"127.0.0.1"); tcpEchoClient.start(); } }
3.3.4对TCP回显程序的优化
在上方的程序中,我们只能对一个客户端进行提供服务,这是因为我们在接收的部分给写成了死循环,如果出现第二个客户端时,第二个客户端只能进行等待。我们如何修改这个程序呢?这是我们就不得不提到多线程了,如果写成多线程的话,就可以处理多个程序了。
程序代码如下(服务端):
package TcpNetWork; 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; 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("服务器启动"); while (true) { //接收到客服端请求 Socket clientSocket=serverSocket.accept(); //将请求发给解决请求的方法 Thread t=new Thread(()->{ try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } }); } } private void processConnection(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 客户端上线了\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); try (InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()){ // 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n // 不是不能做, 而是代码比较麻烦. // 为了简单, 把字节流包装好了更方便的字符流~~ Scanner sc=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true) { //1.接到客服端请求 if (!sc.hasNext()) { System.out.printf("[%s:%d] 客户端下线了\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request=sc.next(); //2.对请求做出响应 String response=process(request); //3.把响应返回给客户端 printWriter.println(response); printWriter.flush(); System.out.printf("[%s:%d] request:%s response:%s ",clientSocket.getInetAddress().toString(), clientSocket.getPort(),request,response); System.out.println(); } } catch (IOException e) { e.printStackTrace(); }finally { clientSocket.close(); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer=new TcpEchoServer(9999); tcpEchoServer.start(); } }
如果我们想要去验证这个程序是否可以对多个客户端提供服务,我们还需要对其修改一点配置,不然我们不能将同一个类运行两次。
1.右键鼠标,选择修改运行配置
2.点开之后,点击修改选项
3.将允许多个实例打勾,这一我们就可以同时运行多个实例了。