很容易搞混,得好好理理
目录
1.网络编程的基本概念
1.1服务器
1.2客户端
2.Socket套接字
2.1概念
2.2分类
3.UDP数据报套接字编程
3.1DatagramPocket API
3.2DatagramSocket API
3.3UDP实现回显服务
4.TCP流套接字编程
4.1ServerSocket API
4.2Socket API
4.3TCP实现回显服务
4.3.1用多线程进行优化
4.3.2用线程池进行优化
①什么是网络编程:
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输。
②请求和响应分别指的什么?
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。
举个例子:
请求和响应在生活中是很好理解的,我手机没电了向别人借一下手机就是一个请求,而别人的回应就是响应。
在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端(也称服务器),可以提供对外服务。通俗可以理解成供应者。
获取服务的一方进程,称为客户端。通俗可以理解成获得者。
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
①流套接字:(使用传输层TCP协议)
a.TCP的特点:
有连接,可靠,面向字节流,全双工
b.分别解释一下以上名词:
有连接:
需要连通,才能交换数据(比如打电话,需要你接了之后才能够和你进行通话)可靠:
传输过程中发送方知道接收方有没有接收数据(打电话,接了电话那么就会接收到数据,否则就不知道是否接收了数据)面向字节流:
以字节为单位进行传输的方式全双工:
一条链路双向通信(无疑,无论是打电话还是发QQ都是一种双向的操作)②数据报套接字:(使用传输层UDP协议)
a.UDP特点:
无连接,不可靠,面向数据报,全双工
b.分别解释一下以上名词:
无连接:
不需要连接,直接就能发送数据(比如发QQ,我不需要得到你的允许就可以直接给你发送)不可靠:
不知道传输的对象有没有接收到信息(发QQ别人未回,我们不知道是已读还是未读)面向数据报:
以数据报为单位进行传输,一次发送/接收都必须是完整的一个数据报或者多个数据报,不能是半个啊,一个半数据报这种
全双工:
一条链路双向通信(无疑,无论是打电话还是发QQ都是一种双向的操作)③原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据。我们不学习原始套接字,简单了解即可。
对客户端发送请求,服务器接收请求并作出回应的过程图(这里是一次请求一次回应)
①什么是DatagramPocket?
DatagramPacket 是 UDP 发送和接收的数据报。每次发送/接收数据,都在传输一个DatagramPocke对象②DatagramPocket的构造方法:
方法签名方法说明 DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf )中,接收指定长度(第二个参数为length) DatagramPacket(byte[]buf, int offset, int length,SocketAddress address) 构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节数组(第一个参数buf )中,从 0 到指定长度(第二个参数length)。 address 指定目的主机的 IP 和端口号注意:
构造 UDP 发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。InetSocketAddress ( SocketAddress 的子类 )构造方法:③ DatagramPocket的方法:
方法签名 方法说明 InetSocketAddress(InetAddress address, int port) 创建一个Socket地址,包含IP地址和端口号
方法签名 方法说明 InetAddressgetAddress() 从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机IP 地址 int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 byte[]getData() 获取数据报中的数据
①什么是DatagramSocket?
创建了一个UDP版本的Socket对象,实质上代表着操作系统中一个Socket文件(更进一步而言是对网卡进行的读写的操作)
②DatagramSocket的构造方法:
方法签名 方法说明 DatagramSocket() 创建一个 UDP 数据报套接字的 Socket ,绑定到本机任意一个随机端口(一般用于客户端) DatagramSocket(intport) 创建一个 UDP 数据报套接字的 Socket ,绑定到本机指定的端口(一般用于服务端)③DatagramSocket的方法:
方法签名 方法说明 void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) void send(DatagramPacketp) 从此套接字发送数据报包(不会阻塞等待,直接发送) void close() 关闭此数据报套接字
①什么是回显服务?
请求的是啥,回应的就是啥的操作
②对回显服务进行代码实现:
相关注意事项注释中已经讲解得非常细了,这里只着重强调一个点
客户端:
import java.io.IOException; import java.net.*; import java.util.Scanner; public class UDPClient { //首先需要有一个DataframSocket实例,这是进行网络编程的前提 private DatagramSocket socket=null; private String serverIP;//IP地址,这里是指服务器传过来的 private int serverPort;//端口号,这里是指服务器传过来的 public UDPClient(String Ip,int port) throws SocketException { socket = new DatagramSocket(); //这里就是用无参版本的构造socket对象,就是让系统随机分配一个端口号 serverIP=Ip;//这个指定的是服务器的IP地址和端口号和客户端没有关系 serverPort=port; } public void start() throws IOException { Scanner input= new Scanner(System.in); while (true){ //首先先从控制台读取到用户输入的信息,然后才能发送 System.out.println("->"); String request = input.next(); //然后把用户写的信息构造成一个UDP请求,才能发送,指定IP和端口号,才知道传到了哪个服务器去 DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIP),serverPort); //然后发送UDP数据报 socket.send(requestPacket); //发送完之后需要再接收从服务器得到的响应数据 //这里仍然需要用一个字节类数组来对返回内容进行接收 DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024); socket.receive(responsePacket); //然后再把响应的数据写成字符串形式,然后打印到控制台 String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8"); System.out.printf("req:%s,resp:%s\n",request,response); } } public static void main(String[] args) throws IOException { UDPClient udpClient=new UDPClient("127.0.0.1",3030); udpClient.start(); } }
服务器:
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UDPServer { //首先需要有一个DataframSocket实例,这是进行网络编程的前提 private DatagramSocket socket = null; public UDPServer(int port) throws SocketException { //这个地方会出现异常的原因 //1.端口号已经被占用了,同一主机的两个程序不能有相同的端口号 //2.2.每个进程能打开的文件个数是有上限的,资源耗尽后是打不开的 socket=new DatagramSocket(port); } public void start() throws IOException { System.out.println("启动服务器!"); //由于UDP的不可靠性,因此可以直接接收从客户端传来的数据,不用进行连接 while(true){ //这个时候就需要等待客户端发来请求,进行接收并作出回应 //因为服务器本身就是被动接收数据的,而不是发送 //构造一个Datagrampocket来对数据报进行发送和接收 // 且接收应该用一个空的byte类型的数组对内容来进行接收(这是DatagramPocket的一个构造方法) DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024); //当没有接收到客户端的请求时,receive()就会阻塞,直到接收到才停止阻塞 socket.receive(requestPacket); //接收到客户端的请求后需要作出回应 //此时已经将接收到的数据放在requestPacket里面了,然后将这里面的数据解析出来,解析成字符串 //new String()方法可以将字符,字节转换为字符串 String request=new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");//也可以指定字符编码 //解析后需要找到一个新的字符串来进行存放,并产生相应 String response = prcoess(request); //对客户端作出响应 //这里不仅要放回响应内容,而且要指定好要发送到的IP地址和端口号 DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress()); //发送过来的是DatagramPacket数据报,返回的也就是这样的数据报,但是发送回去的就不再是一个空的了,而是应该把从上面所得到的响应字符串放到里面然后 socket.send(responsePacket); //可以再手动输出一下客户端的IP地址和端口号 System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response); } } public String prcoess(String request){ return request; } public static void main(String[] args) throws IOException { UDPServer udpServer=new UDPServer(3030); udpServer.start(); } }
③一个简单翻译程序的实现(代码):
客户端没有更改,只是把服务器进行了调整
package UDP; import java.io.IOException; import java.net.SocketException; import java.util.HashMap; public class UDPdictServer extends UDPServer { private HashMap
dict=new HashMap<>(); public UDPdictServer(int port) throws SocketException { super(port); dict.put("cat", "小猫"); dict.put("dog", "小狗"); dict.put("fuck", "卧槽"); dict.put("pig", "小猪"); } @Override public String prcoess(String request) { return dict.getOrDefault(request, "该词无法被翻译!"); } public static void main(String[] args) throws IOException { UDPdictServer server = new UDPdictServer(3030); server.start(); } }
关于TCP的执行过程图
①什么是ServerSocket?
ServerSocket 是创建TCP服务端Socket的API。
②ServerSocket的方法:
方法签名 方法说明 Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket对象,并基于该Socket 建立与客户端的连接,否则阻塞等待 void close()关闭此套接字
①什么是Socket?
Socket 是客户端 Socket ,或服务端中接收到客户端建立连接( accept 方法)的请求后,返回的服务端Socket。②Socket的构造方法:
方法签名 方法说明 Socket(String host, int port) 创建一个客户端流套接字 Socket ,并与对应 IP 的主机上,对应端口的进程建立连接③Socket的方法:
方法签名 方法说明 InetAddress getInetAddress() 返回套接字所连接的地址 InputStream getInputStream() 返回此套接字的输入流 OutputStream getOutputStream() 返回此套接字的输出流
代码实现:(相关细节内容在代码注解中讲得很详细)
客户端:
package TCP; 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 TCPClient { private Socket socket=null; public TCPClient(String serverIP,int serverPort) throws IOException { //对于UDP的socket和TCP的serverSocket来说,构造函数里面给定的端口号,就是指定自己要绑定哪个端口号 //而这里对于TCP的Socket来说,这里指定的IP和端口号是表示自己要与哪一个服务器进行连接,这里的IP和端口号都是服务器的 //调用这个构造方法就会和服务器进行连接,就会使accept的立马进行接通 socket = new Socket(serverIP,serverPort); } //启动客户端 public void start() throws IOException { System.out.println("和服务器连接成功!"); Scanner input = new Scanner(System.in); //因为TCP是流来传输运行的 try(InputStream inputStream=socket.getInputStream()){ try(OutputStream outputStream=socket.getOutputStream()){ while(true){//此时仍然是需要四个步骤 //1.从控制台读取字符串 System.out.println("从控制台读取到的字符串->"); //客户端要输入请求 String request=input.next(); // 2. 根据读取的字符串,将所读的字符串构造为请求, 把请求发给服务器 //用printWriter来对读取的内容进行接收 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request); printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据. //3.读取到服务器来的响应,并对其写入的内容进行解析 //然后从服务器中读取响应,并解析 Scanner respScanner = new Scanner(inputStream); String response = respScanner.next(); //打印先关结果 System.out.printf("req:%s,resp:%s\n",request,response); } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TCPClient tcpClient=new TCPClient("127.0.0.1",9090); tcpClient.start(); } }
服务器:
package TCP; 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 TCPServer { private ServerSocket serverSocket = null; public TCPServer(int port) throws IOException { serverSocket=new ServerSocket(port);//给服务器指定端口号 } public void start() throws IOException { System.out.println("服务器启动"); while(true){ // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话) // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞. // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的. // 进一步讲, serverSocket 就干了一件事, 接电话 //进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作 Socket clientSocket = serverSocket.accept(); //对接收的响应进行处理 processConnection(clientSocket); } } public void processConnection(Socket clientSocket) throws IOException { //IP地址和端口来进行打印 System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); //对客户端发来的请求进行响应处理 // 这里的针对 TCP socket 的读写就和文件读写是一致的 try(InputStream inputStream=clientSocket.getInputStream()){ try(OutputStream outputStream=clientSocket.getOutputStream()) { // 循环的处理每个请求, 分别返回响应 Scanner input = new Scanner(inputStream); while(true){ //读取请求 //已经读取完,不会再输入新的内容 while(!input.hasNext()){ //返回客户端的ip地址以及端口号 System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的 //对请求进行处理 String request=input.next(); // 2. 根据请求, 计算响应 String response = process(request); // 3. 把这个响应返回给客户端 // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下 //读取返回响应所输入的内容用printWriter包裹,返回 PrintWriter printWriter = new PrintWriter(outputStream); 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 { // 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭 try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TCPServer tcpServer=new TCPServer(9090); tcpServer.start(); } }
结果如下:
此时我们会发现,如果是一个客户端的话,与服务器之间的连接是没有问题的,如果是多个客户端,就不能够进行连接。那这到底是为什么呢?
但是显然在我们实际中,这样是不可取的,大大浪费了服务器的资源,所以我们引入了线程池,或者多线程来解决这个问题,代码如下。
代码如下:(对服务器部分进行修改)
package TCP; 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 TCPServer { private ServerSocket serverSocket = null; public TCPServer(int port) throws IOException { serverSocket=new ServerSocket(port);//给服务器指定端口号 } public void start() throws IOException { System.out.println("服务器启动"); while(true){ // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话) // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞. // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的. // 进一步讲, serverSocket 就干了一件事, 接电话 //进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作 Socket clientSocket = serverSocket.accept(); //使用多线程: Thread t = new Thread(()->{ try { processConnection(clientSocket);//这样之后每一个客户端的连接就从串行变成并行的了,然后每一个客户端的连接就可以成功了 } catch (IOException e) { e.printStackTrace(); } }); t.start(); } } public void processConnection(Socket clientSocket) throws IOException { //IP地址和端口来进行打印 System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); //对客户端发来的请求进行响应处理 // 这里的针对 TCP socket 的读写就和文件读写是一致的 try(InputStream inputStream=clientSocket.getInputStream()){ try(OutputStream outputStream=clientSocket.getOutputStream()) { // 循环的处理每个请求, 分别返回响应 Scanner input = new Scanner(inputStream); while(true){ //读取请求 //已经读取完,不会再输入新的内容 while(!input.hasNext()){ //返回客户端的ip地址以及端口号 System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的 //对请求进行处理 String request=input.next(); // 2. 根据请求, 计算响应 String response = process(request); // 3. 把这个响应返回给客户端 // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下 //读取返回响应所输入的内容用printWriter包裹,返回 PrintWriter printWriter = new PrintWriter(outputStream); 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 { // 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭 try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TCPServer tcpServer=new TCPServer(9090); tcpServer.start(); } }
对服务器代码进行修改:
package TCP; 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.*; public class TCPServer { private ServerSocket serverSocket = null; public TCPServer(int port) throws IOException { serverSocket=new ServerSocket(port);//给服务器指定端口号 } public void start() throws IOException { System.out.println("服务器启动"); ExecutorService pool=Executors.newCachedThreadPool(); while(true){ // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话) // accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞. // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的. // 进一步讲, serverSocket 就干了一件事, 接电话 //进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作 Socket clientSocket = serverSocket.accept(); //对接收的响应进行处理 //通过线程池来实现 pool.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } } public void processConnection(Socket clientSocket) throws IOException { //IP地址和端口来进行打印 System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); //对客户端发来的请求进行响应处理 // 这里的针对 TCP socket 的读写就和文件读写是一致的 try(InputStream inputStream=clientSocket.getInputStream()){ try(OutputStream outputStream=clientSocket.getOutputStream()) { // 循环的处理每个请求, 分别返回响应 Scanner input = new Scanner(inputStream); while(true){ //读取请求 //已经读取完,不会再输入新的内容 while(!input.hasNext()){ //返回客户端的ip地址以及端口号 System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的 //对请求进行处理 String request=input.next(); // 2. 根据请求, 计算响应 String response = process(request); // 3. 把这个响应返回给客户端 // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下 //读取返回响应所输入的内容用printWriter包裹,返回 PrintWriter printWriter = new PrintWriter(outputStream); 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 { // 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭 try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TCPServer tcpServer=new TCPServer(9090); tcpServer.start(); } }
最后用TCP实现一个翻译功能(服务器作出一定修改,客户端不改)
package TCP; import java.io.IOException; import java.net.Socket; import java.util.HashMap; public class TcpDictSever extends TCPServer{ private HashMap
map = new HashMap<>(); public TcpDictSever(int port) throws IOException { super(port); map.put("cat","猫"); map.put("pig","猪"); map.put("dog","狗"); } @Override public String process(String request) { return map.getOrDefault(request,"当前词组无法找到!"); } public static void main(String[] args) throws IOException { TcpDictSever tcpDictSever=new TcpDictSever(9090); tcpDictSever.start(); } }