前言
现如今,互联网几乎在我身边随处可见,早已深入我们人类生活中。如今的软件如果没有网络功能,那是很不可思议的事情。(单机游戏都有局域网功能,小时候和朋友联机打的红警就是例子)。许多语言都支持网络编程,我见过C与Java有关网络的程序代码,从中深刻感受到有工具和没工具编代码效率是多么天差地别。就相当于在赛跑,C还在那里造车轮子,Java已经开始用别人给的车开始跑了。
最近一直在学习Java,因此用Java学习网络编程。C/S模式,又称客户端服务器模式。至少用两台计算机来分别充当客户机和服务器角色。C/S模式将应用(对于客户端来说)与服务(对于服务器来说)分离,两边各自开发自己的,两套程序。对于服务器,Java提供一个ServerSocket类来创建服务器,对于客户端,Java提供一个Socket类可以连接服务器。
接下来,就先写三个简单的网络通信的例子(一对一单次通信和一对一多次通信)来熟悉下相关类。
一对一单次通信
一对一单次通信:一个服务器和一个客户端单次通信
服务器
public class OneToOneServer {
public static void main(String[] args) {
int port = 54188;
try {
System.out.println("开始启动服务器...");
ServerSocket server = new ServerSocket(port);
System.out.println("服务器启动成功!");
System.out.println("开始侦听客户端连接请求...");
Socket client = server.accept();
InetAddress inetaddress = client.getInetAddress();
String clientIp = inetaddress.getHostAddress(); //客户端ip地址
String clientName = inetaddress.getHostName(); //客户端名称
System.out.println("发现客户端(ip: " + clientIp + ", 主机名:" + clientName + ")请求连接服务器");
DataInputStream dis = new DataInputStream(client.getInputStream()); //开始建立通信信道
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
System.out.println("通信信道已建立完成");
String message = dis.readUTF();
System.out.println("来自客户端的消息[" + message + "]");
dos.writeUTF("我收到你的消息了");
dis.close();
dos.close();
client.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
public class OneToOneClient {
public static void main(String[] args) {
String serverIp = "127.0.0.1";
//服务器ip地址,因为本地测试所以127.0.0.1
int serverPort = 54188;
//和服务器协商好的
try {
System.out.println("开始连接服务器.....");
Socket socket = new Socket(serverIp, serverPort);
System.out.println("连接成功");
DataInputStream dis = new DataInputStream(socket.getInputStream());
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("你好,我是一个客户端");
String message = dis.readUTF();
System.out.println("来自服务器的消息:" + message);
dis.close();
dos.close();
socket.close();
} catch (UnknownHostException e) { //服务器名称或地址不存在。未知主机异常
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果
#服务器
开始启动服务器...
服务器启动成功!
开始侦听客户端连接请求...
发现客户端(ip: 127.0.0.1, 主机名:127.0.0.1)请求连接服务器
通信信道已建立完成
来自客户端的消息[你好,我是一个客户端]
#客户端
开始连接服务器.....
连接成功
来自服务器的消息:我收到你的消息了
从上面一对一通信程序,希望你能了解到以下几点。
服务器方面:
- ServerSocket的建立需要port(端口号),且这个端口号必须和连接的客户端保持一直,也就是说要和客户端协商好。
- 如果没有客户端连接,程序会卡在accept()那个地方,可以类似于scanf函数,若没有客户端连接则程序会停在accept()那个地方,有客户端连接才会让程序向下进行
- 可以让客户端先不发消息(把发消息那里注释掉),观察情况,发现两个程序都卡住了,都卡在dis.readUTF()这里。所以dis.readUTF()这个方法也是类似于scanf函数,没有收到对端的消息就卡在这,程序不往下走。dis.readUTF()这个方法是读对端发来的消息,是被动的等,一直等,没有就停在这里
- dos.writeUTF()相当于给对端发消息,主动的发消息。
客户端方面:
- Socket的建立需要服务器的ip地址和和服务器协商好的port
- 也需要建立DataInputStream和DataOutputStream
总结一下服务器和客户端都需要DataInputStream和DataOutputStream。它俩相当于什么呢?可以这么说就相当于我们学的通信原理里面的信道那个概念,两端进行信息交换的通信信道;也可以举个现实生活中的例子,就是我们的座机电话,他有两端,上面是听筒端(DataInputStream)和说话端(DataOutputStream)。
一对一多次通信
一对一多次通信:一个服务器和一个客户端多次通信
上述一对一单次通信的代码很简单,我们要从中熟悉一些需要用的类。但是,我相信肯定不是上面我们最终的目的吧?不可能通信只通信一句话吧?所以来进一步实现一个客户端和一个服务器多次通信。
如何进行多次通信呢?经过思考只要通信信道不关闭,就可以多次发送消息。但不能一直通信下去,要有办法停止,也就是可控性。所以定发特定的字符串就停止下来。
服务器
public class SimpleServer {
private int port;
private ServerSocket serverSocket;
private DataInputStream dis;
private DataOutputStream dos;
private Socket client;
public SimpleServer() throws IOException {
port = 54188;
System.out.println("开始建立服务器");
serverSocket = new ServerSocket(port);
System.out.println("服务器建立完成");
}
private void close() {
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
try {
if (client != null && !client.isClosed()) {
client.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
client = null;
}
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
serverSocket = null;
}
}
public void listeningClient() throws IOException {
System.out.println("开始侦听客户端连接请求");
client = serverSocket.accept();
System.out.println("客户端[" + client.getInetAddress().getHostAddress() + ", " + client.getInetAddress().getHostName()
+ "]连入服务器");
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
String message = "";
System.out.println("接受来自客户端的消息...");
while (!message.equalsIgnoreCase("byebye")) {
message = dis.readUTF();
System.out.println("来自客户端的消息:" + message);
dos.writeUTF("[" + message + "]");
}
close();
}
}
客户端
public class SimpleClient {
private Socket socket;
private String ip;
private int port;
private DataInputStream dis;
private DataOutputStream dos;
public SimpleClient() throws UnknownHostException, IOException {
ip = "127.0.0.1";
port = 54188;
System.out.println("开始连接服务器...");
socket = new Socket(ip, port);
System.out.println("连接服务器成功");
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
}
public void talking() {
Scanner in = new Scanner(System.in);
String message = "";
String messageFromServer;
try {
while(!message.equalsIgnoreCase("byebye")) {
message = in.nextLine();
dos.writeUTF(message);
messageFromServer = dis.readUTF();
System.out.println("来自服务器的消息:" +messageFromServer);
}
} catch (IOException e) {
e.printStackTrace();
}
in.close();
close();
}
private void close() {
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
运行代码。注意:要先运行服务器,在运行客户端
public class Test {
public static void main(String[] args) {
try {
new SimpleServer().listeningClient();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
try {
new SimpleClient().talking();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
显示结果如下
#服务器
开始建立服务器
服务器建立完成
开始侦听客户端连接请求
客户端[127.0.0.1, 127.0.0.1]连入服务器
接受来自客户端的消息...
来自客户端的消息:你好
来自客户端的消息:我是一个客户端
来自客户端的消息:byebye
#客户端
开始连接服务器...
连接服务器成功
你好
来自服务器的消息:[你好]
我是一个客户端
来自服务器的消息:[我是一个客户端]
byebye
来自服务器的消息:[byebye]
一对多多次通信
一对多多次通信:一个服务器和多个客户端的多次通信
前面的代码不是我们最终的目的,如果一个服务器只能接入一个客户端,你说他还能称为服务器吗?服务器作为网络节点中心管理者,要实现接入多个客户端的功能。经过分析上述代码得知之所以只接入了一个客户端的原因是,accept()方法只运行了一次,要想接入多个客户端,必须让accept()方法重复执行,我们可以把它放在循环里,条件是服务器关闭与否。但是还是有问题,前面说过accept()方法相当于scanf函数,没有接入它的客户端,程序会卡在那里阻止下面通信的代码。解决方法:用线程。服务器accept()侦听客户端连接用线程并行去跑,不要影响我下面的代码。
同时分析上述代码我们可以知道每连接一个新的客户端都要新造一个独立的通信信道,这是当然啦,你要和服务器建立联系,就得造通信信道,该通信信道是只有你和服务器用的,独立的,不能让别人用。两端都要有通信信道(DataInputStream和DataOutputStream)所以我们最好把它单独拎出来做成一个类。同时我们知道dis.read()方法也相当于scanf函数,程序卡在这里不会运行以后的代码,你等待对端消息不能干扰我主动给对端发消息吧。因此,也用线程解决。
只有做到以上两点,才能完成一对多多次通信。才能实现服务器边听(侦听客户端连接)边说(给对端发消息)!可以用以下图示来说明。
先写双方都需要建立的通信信道类Communcation
public abstract class Communcation implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private volatile boolean goon; //volatitle关键字:拒绝内存优化,这是线程安全相关问题
public Communcation(Socket socket) {
this.socket = socket;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
goon = true;
new Thread(this, "通信层").start();
} catch (IOException e) {
e.printStackTrace();
}
}
void send(String message) {
try {
dos.writeUTF(message);
} catch (IOException e) {
close();
}
}
protected abstract void dealPeerMessage(String message);
protected abstract void dealPeerAbnoramlDrop();
@Override
public void run() {
String message = null;
while (goon) {
try {
message = dis.readUTF();
dealPeerMessage(message); //处理对端消息具体做法让外面来决定
} catch (IOException e) {
if (goon == true) {
dealPeerAbnoramlDrop(); //当goon还是true的时候,说明是对端异常掉线,做法让外面来决定
}
close();
}
}
close();
}
void close() {
goon = false;
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
}
}
服务器
public class MulServer implements Runnable {
private ServerSocket server;
private int port;
private boolean goon;
public MulServer() {
port = 54188;
}
public void startUp() throws IOException {
if (goon == true) {
System.out.println("服务器已经启动,请勿重复启动");
return;
}
System.out.println("开始建立服务器...");
server = new ServerSocket(port);
System.out.println("服务器建立成功");
goon = true;
System.out.println("开始侦听客户端连接...");
new Thread(this, "服务器").start();
}
public void shutDown() {
if (goon == false) {
System.out.println("服务器已经宕机,请勿重复宕机");
return;
}
close();
System.out.println("服务器正常宕机");
}
public boolean isStartup() {
return goon;
}
@Override
public void run() {
while (goon) {
try {
Socket client = server.accept();
System.out.println("客户端[" + client.getInetAddress().getHostAddress() +
client.getInetAddress().getHostName() + "]连入服务器");
new Communcation(client) {
@Override
protected void dealPeerMessage(String message) {
if (message.equalsIgnoreCase("byebye")) {
System.out.println("客户端正常下线");
close();
return;
}
System.out.println("收到来自客户端的消息:" + message);
send("[" + message +"]");
}
@Override
protected void dealPeerAbnoramlDrop() {
System.out.println("客户端异常掉线");
}
};
} catch (IOException e) {
close();
}
}
}
private void close() {
goon = false;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
}
客户端
public class MulClient {
private Socket socket;
private int port;
private String serverIp;
private Communcation communcation;
public MulClient() {
port = 54188;
serverIp = "127.0.0.1";
}
public void send(String message) {
if (communcation == null) {
return;
}
communcation.send(message);
}
public void close() {
if (communcation == null) {
return;
}
communcation.close();
}
public void connectToServer() throws UnknownHostException, IOException {
socket = new Socket(serverIp, port);
communcation = new Communcation(socket) {
@Override
protected void dealPeerMessage(String message) {
System.out.println("来自服务器的消息:" + message);
}
@Override
protected void dealPeerAbnoramlDrop() {
System.out.println("服务器异常宕机,服务停止");
}
};
}
}
服务器测试
public class ServerTest {
public static void main(String[] args) {
MulServer mulServer = new MulServer();
Scanner in = new Scanner(System.in);
String command = null;
boolean finshed = false;
while (!finshed) {
command = in.next();
if (command.equalsIgnoreCase("st")) {
try {
mulServer.startUp();
} catch (IOException e) {
e.printStackTrace();
}
} else if (command.equalsIgnoreCase("sd")) {
mulServer.shutDown();
} else if (command.equalsIgnoreCase("x")) {
if (!mulServer.isStartup()) {
finshed = true;
} else {
System.out.println("服务器尚未宕机!");
}
}
}
in.close();
}
}
客户端测试
public class ClientTest {
public static void main(String[] args) {
MulClient client = new MulClient();
boolean finished = false;
try {
client.connectToServer();
Scanner in = new Scanner(System.in);
String message = "";
while (!finished) {
message = in.next();
client.send(message);
if (message.equalsIgnoreCase("byebye")) {
client.close();
finished = true;
}
}
in.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
先运行服务器测试代码,在运行客户端测试代码。进行测试结果如下
#服务器
st
开始建立服务器...
服务器建立成功
开始侦听客户端连接...
客户端[127.0.0.1127.0.0.1]连入服务器
客户端[127.0.0.1127.0.0.1]连入服务器
客户端[127.0.0.1127.0.0.1]连入服务器
收到来自客户端的消息:123
收到来自客户端的消息:456
收到来自客户端的消息:789
客户端正常下线
客户端异常掉线
客户端正常下线
sd
服务器正常宕机
x
#客户端1
123
来自服务器的消息:[123]
byebye
#客户端2 故意强制关闭
456
来自服务器的消息:[456]
#客户端3
789
来自服务器的消息:[789]
byebye
总结
一对多多次通信就完成了。服务器可以接入多个客户端,客户端可以发消息给服务器。但是,这就完了吗?这就是我们想要的吗?我并不认为就完了,我们做的功能太简陋了,完全不够用。同时思考可不可以将底层通信的代码做成工具,做成普适性工具,这样以后开发不同需求的C/S程序就会方便很多。这就是框架的含义,也就是我们要做的工具CSFramework。
【Java框架】保姆级教你写出简易框架——CSFramework
UDP通信
前面的方式是TCP,像打电话,需要建立通信信道,只有两边通信信道都建立好了才可以进行信息的交互。而下面要说的是UDP,像发信息,不需要经过连接,需要知道对方的ip和port。
需要两个类完成UDP传输。DatagramPacket和DatagramSocket。
发送方代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class UdpSenderDemo {
public static void main(String[] args) {
try {
DatagramSocket sender = new DatagramSocket(); //发送方直接new
byte[] buffer = new byte[1024];
String message = "你好,我是一个udp包,你可以收到吗?";
buffer = message.getBytes();
DatagramPacket data = new DatagramPacket(buffer, 0, buffer.length, InetAddress.getLocalHost(), 54188);
//发送时要把发送的ip和port说清楚
sender.send(data);
sender.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
其直接运行都不会出错,更加说明了发UDP包不需要连接,我只管发送,发送完后,到没到我都不知道,只管发。
接收方代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpReciverDemo {
public static void main(String[] args) {
try {
DatagramSocket reciver = new DatagramSocket(54188); //接收方实例化需要开自己的port
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
//接收时,不需要ip和port,直接接受网络消息到缓冲区即可
reciver.receive(packet); //阻塞接受
System.out.println(new String(packet.getData(), 0, packet.getLength()));
reciver.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
从这个例子明白了没有客户端和服务器之分,都是DatagramScoket。叫发送端和接收端。