今天,人们广泛使用计算机网络来实现多台计算机之间的联系与数据的交换;而如果想实现位于同一个网络中的计算机之间进行通信,就需要通过编写网络程序来实现,这就是所谓网络编程。即对网络上的主机,通过不同的进程,使用编程的方式来实现网络间的数据传输。
网络通信协议,实际上就是计算机网络间连接和通信的规则;与交通规则类似,只有通信的双方都遵守这一协议,才能顺利完成数据的交换和信息的交流。
网络通信的协议有许多,但目前应用最为广泛的是TCP/IP协议。这里不对协议的内容做展开阐述,只是基于网络通信协议学习如何进行网络编程;
客户端:获取服务的一方,主动发送网络数据;
服务器:提供服务的一方,被动接收网络数据;
发送端:网络通信中的源主机,数据的发送方;
接收端:网络通信中的目的主机,数据的接收方;
请求:客户端给服务器发送的数据;
响应:服务器返回给客户端的数据;
一问一答:客户端发送一个请求,服务器返回一个响应;
多问一答:客户端发送多个请求,服务器返回一个响应;
一问多答:客户端发送一个请求,服务器返回多个响应;
多问多答:客户端发送多个请求,服务器返回多个响应;
Socket套接字是基于TCP/IP协议的网络通信的基本操作单元,通过对网络连接进行高级的抽象,提供了操作网络连接的便利性,Socket就像是网络编程的标准。使用Socket接口,即使是在不同系统上运行不同的TCP/IP,也同样可以成功运行;
套接字依据传输层协议可以划分为3类:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
public class UdpServer {
//创建UDP服务器,首先打开一个socket文件
private DatagramSocket socket=null;
public UdpServer(int port) throws SocketException {
//创建UDP服务器,指定端口,可以发送和接收数据报
socket=new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//使用循环不断接收客户端UDP数据报
while(true){
//使用DatagramPacket的构造方法,创建一个字节数组用于接收数据,
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
//使用receive方法从此套接字接收数据报
socket.receive(requestPacket);
//对客户端发来的请求进行解析,把DatagramPacket转化成String类型
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//对请求进行处理,进行响应
String response=process(request);
//构造响应对象,为DatagramPacket对象,
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 req){
return req;
}
public static void main(String[] args) throws IOException {
//实例化一个UDP服务器,同时指定端口
UdpServer server=new UdpServer(8000);
server.start();
}
}
由于UDP是一种面向无连接的协议,因此,通信时发送端和接收端不用建立连接;
JDK中提供了一个DatagramPacket类,用于封装UDP通信中发送或接收的数据;
下面是客户端一方的Socket编程:
import jdk.nashorn.internal.ir.WhileNode;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpClient {
private DatagramSocket socket=null;
public UdpClient() throws SocketException {
//创建UDP客户端,不需要手动指定端口号,一般由操作系统自动分配
socket=new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while (true){
System.out.print("--->");
//从控制台读取数据
String request=scanner.next();
//创建一个要发送的数据包,包括发送的数据,数据长度,接收端的IP地址以及端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName("127.0.0.1"), 8000);
//发送数据包给服务器
socket.send(requestPacket);
//创建数组用于读取数据
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
//接收数据报,
socket.receive(responsePacket);
//将响应数据转化为String
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 {
UdpClient client=new UdpClient();
client.start();
}
}
DatagramSocket类用于发送和接收DatagramPacket数据包,就类似于“码头”的作用;
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.security.spec.RSAOtherPrimeInfo;
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){
//返回一个与客户端连接的对象
Socket clientSocket=serverSocket.accept();
processConnect(clientSocket);
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(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 scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
ServerSocket类是JDK为开发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() throws IOException {
//创建客户端,通过IP地址和端口号与服务器建立连接
socket=new Socket("127.0.0.1",8000);
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scanner1=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
//循环实现输入
while (true){
System.out.println(">---");
//从控制台读取输入数据
String request=scanner.next();
//发送请求给服务器
printWriter.println(request);
//刷新缓冲区
printWriter.flush();
//从服务器读取响应
String response=scanner1.next();
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
}
public static void main(String[] args) throws IOException {
TcpClient client=new TcpClient();
client.start();
}
}
Socket类用于实现TCP客户端程序;
短连接:即每次接收到数据并返回响应后便关闭连接,一次连接只进行一次数据交互;
长连接:即交互双方不停进行交互,不关闭连接;
短连接与长连接的区别:
由于短连接在每次请求和响应时,都要建立连接再关闭连接,因此长连接的效率相对更高;
短连接一般是客户端主动向服务端发送请求,长连接则两方都可以主动发送请求;
短连接更加适用于客户端请求频率较低的场景,而长连接更加适用于客户端与服务器交互频繁的场景;
上面所提供的TCP版Socket编程中,关于服务器的实现,仅仅是提供了一种单线程的方式。但是在实际的应用场景中,需要一台服务器同时为多台客户端提供服务的情况时有发生,因此我们尝试实现一种多线程版的服务器端程序:
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.security.spec.RSAOtherPrimeInfo;
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){
//返回一个与客户端连接的对象
Socket clientSocket=serverSocket.accept();
//多线程版
Thread t=new Thread(()->{
try{
processConnect(clientSocket);
} catch (IOException e){
e.printStackTrace();
}
});
t.start();
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(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 scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
客户端程序与上面TCP版的Socket编程一致,下面是实际的运行情况:
解决了为多台客户端服务的问题,我们的实现方式又带来了新的问题,在有众多客户端请求的情况下,使用普通的线程创建方式,必定会频繁的创建和销毁线程,因而有可能会加大系统的开销,所以我们可以尝试线程池的方式:
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.security.spec.RSAOtherPrimeInfo;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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 service= Executors.newCachedThreadPool();
while (true){
//返回一个与客户端连接的对象
Socket clientSocket=serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnect(clientSocket);
}catch (IOException e){
e.printStackTrace();
}
}
});
}
}
/*
* 为与客户端建立了连接的对象提供服务
* */
public void processConnect(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 scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//读取请求并解析
String request=scanner.next();
//根据请求进行响应
String response=process(request);
//返回响应给客户端
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
}finally {
clientSocket.close();
}
}
/*
* 对请求做出响应
* */
public String process(String req){
return req;
}
public static void main(String[] args) throws IOException {
TcpServer server=new TcpServer(8000);
server.start();
}
}
最后是关于IDEA在不结束程序的情况下再打开同一程序的方法:
首先在这里的下拉框选择要同时打开哪个程序:
点击下面选中的编辑栏:
来到这个界面,点击蓝色字体Modify options: