什么是网络编程
网络编程指的是,**网络上的主机,不同的进程,通过网络的方式实现通信。**同一个主机但是不同进程之间通信也是网络编程,但是我们主要编程对象是不同主机至今啊的通信。
网络编程实际上就是把传输层和应用层进行封装,然后利用java提供的api进行通过,代码的形式交给传输层然后进行通信。
网络编程的基本概念
客户端和服务器
- 客户端:发起通信的一方,实际上就是我们平时的应用之类的。
- 服务器:接受数据的一方,接收数据并进行处理。
客户端服务器的定义实际上是谁发起了通信,谁接受了数据。
请求和响应
**请求:**request,客户端给服务器发送的数据。
**响应:**response,服务器返回给客户端的数据。
Socket关键字
Socket是系统提供的方式用于网络通信,网络通信常常基于Socket关键字。
TCP和UDP是传输层的两个重要的协议。
TCP的特点
- 有连接(必须要双方都接通了才能进行通信,需要三次握手四次挥手)。
- 可靠传输(可以知道对方是否接收到了数据)。
- 面向字节流(网络中的传输数据是字节模式,以字节为单位)。
- 全双工(可以双向通信)。
UDP的特点
- 无连接(就是类似于QQ直接发出去,无需等待对方建立连接)。
- 不可靠传输(对方就算对方没有接收到,发送端也不知道有没有对方是否接收到)。
- 面向数据报(单位是数据报)。
- 全双工(可以双向通信)。
UDP编程:
1.DatagramSocket
Datagramsocket是UDP Socket的关键方法,用来发送和接受UDP数据。
构造方法:
重要方法:
2.DatagramPacket
DatagramPacket是UDP Socket发送数据的数据报(每次接收和发送数据的基本单位就是 数据报)。
构造方法:
3.UDP回显服务器
服务器和客户端都要指定一个端口号,但是一般服务器的端口号要显式指定,客户端不能显式指定,系统会自动分配。服务器需要把端口号明确下来,需要让别人找到。客户端的端口号不能指定,因为有可能被别人占用了(避免端口号冲突),交给系统分配。
服务器的端口号在程序员手里,服务器的哪些端口号被使用了,程序员都知道的。客户端在客户上面。一个服务器程序需要长时间运行。
new DatagramPacket()用来承载从网卡这边读到的数据,读到数据需要指定一个内存空间来保存这个数据。socket(网卡)读取数据,,并且保存到requestpacket里面。
receive会阻塞,直到客户端发送数据。
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//服务器代码,创建一个DatagramSocket,后续操作网卡
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
this.socket = new DatagramSocket(port);
//socket=new DatagramSocket();这是让系统分配的方法。
}
public void start() throws IOException {
while (true){
//读取请求并且解析
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
//从网卡中读取数据,并且存储在packet中,有数据才会接受不然就会阻塞
socket.receive(requestPacket);
//拿到数据,并且放入到String中,取区间内的字节,构造成String,这里的getlength实际上不是4096,是收到的数据的真实长度
String request=new String(requestPacket.getData(),0, requestPacket.getLength());
//根据请求计算响应!!!!一般服务器最重要的
String response=process(request);
//将响应写回去
//UDP是无连接的,每次都要指定数据要发给谁
//构造数据报,需要指定数据内容,也要指定发给谁
//不能直接getlength,获取字符为单位的如果都是英文单词,那字符字节一样,中午不一样,网络传输都是字节为单位
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req=%s resp=%s",requestPacket.getAddress().toString(),responsePacket.getPort(),request,response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
udpEchoServer.start();
}
}
对于客户端,服务器的端口号可以由系统随机分配,但需要知道服务器的IP地址及端口号,不然就不知道发送数据给谁。
-
客户端发送数据
-
构造数据报通过socket发送给服务器
-
服务器进行读取并且返回给客户端
-
客户端输出发送的响应
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UdpEchoClient {
private DatagramSocket socket;
private String address;
private int port;public UdpEchoClient(String address, int port) throws SocketException { this.address = address; this.port = port; socket=new DatagramSocket();//这里表示服务器的随机端口创建 } public void start() throws IOException { System.out.println("客户端启动"); Scanner input=new Scanner(System.in); while (true){ //没有输入数据的时候就跳出循环 if(!input.hasNext()){ break; } //读取所有的数据 String request=input.next(); //将内容构造成datagrampacket发送出去,并且需要找到对方的ip地址和端口号 DatagramPacket datagramPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(address),port); //通过网卡进行发送数据报 socket.send(datagramPacket); //读取服务器内容并且显示在客户端。 DatagramPacket responseDatagramPacket=new DatagramPacket(new byte[4096],4096); socket.receive(responseDatagramPacket); String response=new String(responseDatagramPacket.getData(),0,responseDatagramPacket.getLength()); System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9090); udpEchoClient.start(); }
}
但是实际上这个程序不能跨主机通信,如果想要实现跨主机通信,就要把程序部署到云服务器上面。
4.UDP翻译回显服务器
基于上述回显服务器,还可以实现出一些其他带有一点业务逻辑的服务器。
进行业务逻辑的修改实际上就是进行对回显服务器的继承,再实现更多的细节和代码。
上述的操作是在process的代码中实现的,我们只要进行继承然后重写方法就可以达到汉译英的效果了。
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDireServer extends UdpEchoServer{
private Map<String,String> map;
public UdpDireServer(int port) throws SocketException {
super(port);
map = new HashMap<>();
map.put("cat", "小猫");
map.put("bear", "小熊");
}
@Override
public String process(String request) {
if(map.get(request)!=null){
return map.get(request);
}
return request;
}
public static void main(String[] args) throws IOException {
UdpDireServer udpDireServer=new UdpDireServer(9090);
udpDireServer.start();
}
}
观察一下运行结果,发现没有问题。
以上就是UDP的回显服务器的开发和运行了。
TCP编程:
1.ServerSocket
ServerSocket是创建TCP服务端Socket的API(只能给服务器使用)。
构造方法:
重要方法:
2.Socket
Socket 类用于创建客户端 Socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket. (服务器端和客户端都能使用)
构造方法:
重要方法:
3.TCP回显服务器程序
TCP和UDP的区别就是TCP是有连接的,就和打电话一样需要一方接通另外一方才能进行通话。所以要等待客户端发起请求后,服务器确认接通之后,才可以进行通信,TCP的首要任务就是建立连接。
和UDP回显服务器一样,对于这里的服务器,同样需要指定端口号创建TCP服务器端Socket,即ServerSocket。
- 服务器启动之后,需要通过accept方法来监听当前端口。
- 成功建立连接之后,就可以返回一个Socket方法,这个对象保存了对端的信息,即客户端信息,可以用来接收和发送请求等(TCP是面向字节流),可以通过该方法来发送和接收数据。
后续流程和UCP回显服务器一致。此处由于每有一个客户端连接,就会有一个clientSocket,这里消耗的Socket会越来越多,因此每当一个客户端连接结束,就需要释放这个clientSocket。
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.time.temporal.IsoFields;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//ExecutorService threadPool = Executors.newCachedThreadPool();
while (true){
while (true) {
//监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
}
private void processConnection(Socket clientSocket) throws IOException {
//返回ip地址和对应的端口
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
try(OutputStream outputStream=clientSocket.getOutputStream();
InputStream inputStream=clientSocket.getInputStream()) {
//不断读取输入的数据
while (true){
Scanner scanner=new Scanner(inputStream);
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端下线\n",
clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
//next是等到/n才结束,也就是用户输入换行(回车)
String request=scanner.next();
String response=process(request);
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] request:%s response:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
}finally {
clientSocket.close();
}
}
private String process (String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
对于客户端,需要指定服务器的IP和端口号建立连接。使用 Socket(String host, int port) 创建Socket的时候,就开始发起与对应服务器建立连接的请求了。
实际上TCP回显服务器和UDP很相似。,但是TCP是面向字节流。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class TcpEchoClient {
private Socket clientSocket;
public TcpEchoClient(String serverAddress, int serverPort) throws IOException {
clientSocket=new Socket(InetAddress.getByName(serverAddress),serverPort);
}
public void start() {
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream();
Scanner scanner=new Scanner(System.in)) {
while (true){
System.out.print("->");
String request=scanner.next();
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//读取服务器发送的数据
Scanner inputScanner=new Scanner(inputStream);
String response=inputScanner.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
tcpEchoClient.start();
}
}
在这里我们需要先运行服务器,再运行客户端,通常服务器都需要先启动,不然客户端会因为连接不上服务器而报错。
实际上这里还存在一个问题。这里的服务器只能给先获取连接的客户端提供服务,如果其他客户端想访问则会失败。
分析过程:
- 第一个客户端连上服务器之后,服务器就会从accept这里返回(解除阻塞),然后进入到processConnection方法中。
.接下来服务器就会在processConnection循环处理客户端的请求,只有当客户端退出之后,连接结束,才会退出循环。 - 而服务器在循环处理客户端请求的时候,第二个客户端发起连接请求,而服务器这里并不能执行到accept。因此并不能成功连接,只有当客户端退出,才会执行回到accept进行连接。
第二个客户端之前发的请求为什么能被立即处理?
- 当前TCP在内核中,每个 socket 都是有缓冲区的。客户端发送的数据通过客户端代码,已经写入到服务器的缓冲区了,这里数据确实发送了,只不过数据在服务器的接收缓冲区中。
- 一旦第一个客户端退出,回到第一层循环,执行accept连接操作,后续processConnection方法里的 next 就能把之前缓冲区的内容给读出来。
实际上可以通过多线程来解决此问题,为每个访问的客户端都创建一个线程,使其可以通过单独的线程来进行访问服务器。
4. 服务器引入多线程
//多线程
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
但是实际上,像这样频繁创建和销毁线程对服务器来说是一个不小的开销。
- 每有一个客户端连接,就会创建一个新的线程,每当这个客户端结束,就要销毁这个线程。
- 如果客户端比较多,并且频繁连接、关闭,就会使服务器频繁创建和销毁线程
因此我们使用了线程池。
5.服务器引入线程池
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
6.TCP字典服务器
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer{
Map<String,String> map=new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
map.put("cat","小猫");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"未查找到单词");
}
public static void main(String[] args) throws IOException {
TcpDictServer tcpDictServer=new TcpDictServer(9090);
tcpDictServer.start();
}
}