一、TCP通信
1.1 Socket原理
1.1.1 Socket简介
socket通常称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。
应用程序通常通过“套接字”向网络发出请求或者应答网络请求。Socket和ServerSocket类位于java.net包中。ServerSocket用于服务端,Socket是建立网络连接时使用的,在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。
1.1.2 获取本地地址和端口号
java.net.Socket为套接字类,其提供了很多方法,其中我们可以通过Socket获取本地的地址以及端口号。
- int getLocalPort(),该方法用于获取本地使用的端口号。
- InetAddress getLocalAddress(),该方法用于获取套接字绑定的本地地址。
使用InetAddress获取本地的地址方法:
- String getCanonicalHostName(),获取此IP地址的完全限定域名。
- String getHostAddress(),返回IP地址字符串(以文本表现形式)。
1.1.3 获取远端地址和端口号
通过Socket获取远端的地址以及端口号。
- int getPort(),该方法用于获取远端使用的端口号。
- InetAddress getInetAddress(),该方法用于获取套接字绑定的远端地址。
1.1.4 获取网络输入流和网络输出流
通过Socket获取输入流与输出流,这两个方法是使用Socket通讯的关键方法。
- InputStream getInputStream(),该方法用于返回此套接字的输入流。
- OutputStream getOutputStream(),该方法用于返回此套接字的输出流。
1.1.4 close方法
当使用Socket进行通讯完毕后,要关闭Socket以释放系统资源。
- close(),关闭此套接字。当关闭了该套接字后也会同时关闭由此获取的输入流与输出流。
1.2 Socket通信模型
1.2.1 Server端ServerSocket监听
java.net.ServerSocket是运行于服务端应用程序中。通常创建ServerSocket需要指定服务端端口号,之后监听Socket的连接:
//创建ServerSocket并申请服务端口8088
ServerSocket server = new ServerSocket(8088);
//方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket
Socket socket = server.accept();
1.2.2 Client端Socket连接
当服务端创建ServerSocket并通过accept()方法侦听后,我们就可以通过在客户端应用程序中创建Socket来向服务端发起连接。
需要注意的是,创建Socket的同时就发起连接,若连接异常会抛出异常。
//参数1:服务端的IP地址,参数2:服务端的服务端口
Socket socket = new Socket("localhost",8088);
1.2.3 Server端多线程模型
因为需要处理多客户端,所以服务端要周期性循环调用accept方法,但该方法会产生阻塞,所以与某个客户端的交互就需要使用多线程来并发处理。
下面是一个使用TCP通信实现的聊天室:
package chat;
/**
* 聊天室服务端
* @author Administrator
*
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Server {
/*
* 运行在服务端的ServerSocket,主要负责:
* 1、向系统申请服务端口
* 客户端就是通过这个端口与之连接的
* 2、监听申请的服务端口,当一个客户端通过该端口尝试建立连接时,
* ServerSocket会在服务端创建一个Socket与客户端建立连接。
*/
private ServerSocket server;
//保存所有客户端输出流的集合
private List<PrintWriter> allOut;
/*
* 用来初始化服务端
*/
public Server() throws Exception{
/*
* 初始化的同时申请服务端口
*/
server = new ServerSocket(8088);
allOut = new ArrayList<PrintWriter>();
}
/*
* 将给定的输出流存入共享集合
*/
private synchronized void addOut(PrintWriter out) {
allOut.add(out);
}
/*
* 将给定的输出流从共享集合中删除
*/
private synchronized void removeOut(PrintWriter out) {
allOut.remove(out);
}
/*
* 将给定的消息发送给所有客户端
*/
private synchronized void sendMessage(String message) {
for (PrintWriter out:allOut) {
out.println(message);
}
}
/*
* 服务端开始工作的方法
*/
public void start() {
try {
/*
* ServerSocket的accept方法是一个阻塞方法,作用是
* 监听服务端口,直到一个客户端连接。并创建一个Socket,
* 使用该Socket即可与刚连接的客户端进行交互。
*/
while (true) {
System.out.println("等待客户端连接...");
Socket socket = server.accept();
System.out.println("一个客户端连接了!");
/*
* 启动一个线程,来完成与该客户端的交互
*/
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Server server = new Server();
server.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("服务端启动失败!");
}
}
/*
* 该线程负责处理一个客户端的交互
*/
class ClientHandler implements Runnable{
private Socket socket;//该线程处理的客户端的socket
private String host;//客户端的地址信息
private String nickname;//该用户的昵称
public ClientHandler(Socket socket) {
this.socket = socket;
/*
* 通过Socket可以获取远端计算机的地址信息
*/
InetAddress address = socket.getInetAddress();
//获取IP地址
host = address.getHostAddress();
}
public void run() {
PrintWriter pw = null;
try {
//System.out.println(host+"上线了!");
/*
* socket提供的方法
* InputStream getInputStream()
* 该方法可以获取一个输入流,从该流读取的数据就是从远端计算机
* 发送来的。
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
//首先读取一行字符串为昵称
nickname = br.readLine();
sendMessage(nickname+"上线了!");
/*
* 通过Socket创建输出流用于将消息发送给客户端
*/
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
pw = new PrintWriter(osw,true);
/*
* 将该客户端的输出流存入到共享集合中
*/
addOut(pw);
String message = null;
/*
* br.readLine()在读取客户端发送过来的消息时,
* 由于客户端断线,而其操作系统的不同,这里读取
* 后的结果不同:
* 当windows的客户端断开时,br.readLine()会抛出异常。
* 当Linux的客户端断开时,br.readLine()会返回null。
*/
while ((message = br.readLine()) != null) {
//System.out.println(host+":"+message);
//pw.println(host+":"+message);
//广播消息
sendMessage(nickname+":"+message);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/*
* 处理当前客户端断开后的逻辑
*/
//将该客户端的输出流从共享集合中删除
removeOut(pw);
//System.out.println(host+"下线了!");
sendMessage(nickname+"下线了!");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package chat;
/**
* 聊天室客户端
* @author Administrator
*
*/
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
/*
* java.net.Socket
* 封装了TCP协议,使用它就可以基于TCP协议进行网络通讯
* Socket是运行在客户端的
*/
private Socket socket;
/*
* 构造方法,用来初始化客户端
* 实例化Socket的时候需要传入两个参数:
* 1、服务端地址:通过IP地址可以找到服务端的计算机
* 2、服务端端口:通过端口可以找到服务端计算机上的服务端应用程序
* 实例化Socket的过程就是连接的过程,若远程计算机没有响应会抛出异常。
*/
public Client() throws Exception{
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("已与服务端建立连接!");
}
/*
* 启动客户端的方法
*/
public void start() {
try {
Scanner scanner = new Scanner(System.in);
/*
* 先要求用户输入一个昵称
*/
String nickName = null;
while (true) {
System.out.println("请输入用户名:");
nickName = scanner.nextLine();
if (nickName.length() > 0) {
break;
}
System.out.println("输入有误!");
}
System.out.println("欢迎你"+nickName+"!开始聊天吧!");
/*
* socket提供的方法:
* OutputStream getOutputStream()
* 获取一个字节输出流,通过该流写出的数据会被发送至
* 远端计算机。
*/
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
PrintWriter pw = new PrintWriter(osw,true);
/*
* 先将昵称发送给服务端
*/
pw.println(nickName);
/*
* 启动读取服务端发送过来消息的线程
*/
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.start();
while (true) {
/*
* 将字符串发送至服务端
*/
pw.println(scanner.nextLine());
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Client client = new Client();
client.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("客户端启动失败!");
}
}
/*
* 该线程用来读取服务端发送过来的消息
* 并输出到客户端控制台显示。
*/
class ServerHandler implements Runnable{
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while ((message = br.readLine()) != null) {
System.out.println(message);
}
} catch (Exception e) {
}
}
}
}
二、UDP通信
2.1 DatagramPacket
2.1.1 构建接收包
- DatagramPacket(byte[] buf,int length),将数据包中length长的数据装进buf数组。
- DatagramPacket(byte[] buf,int offset,int length),将数据包中从offset开始,length长的数据装进buf数组。
2.1.2 构建发送包
-DatagramPacket(byte[] buf,int length,InetAddress clientAddress,int clientPort),从buf数组中,取出length长的数据创建数据包对象,目标是clientAddress地址,clientPort端口,通常用来发送数据给客户端。
-DatagramPacket(byte[] buf,int offset,int length,InetAddress clientAddress,int clientPort),从buf数组中,取出offset开始的、length长的数据创建数据包对象,目标是clientAddress地址,clientPort端口,通常用来发送数据给客户端。
2.2 DatagramSocket
2.2.1 服务端接收
- DatagramSocket(int port),创建实例,并固定监听port端口的报文,通常用于服务端。
- receive(DatagramPacket d),接收数据报文到d中,receive方法产生“阻塞”。
2.2.2 客户端发送
无参的构造方法DatagramSocket(),通常用于客户端编程,它并没有特定监听的端口,仅仅使用一个临时的。程序会让操作系统分配一个可用的端口。
- send(DatagramPacket dp),该方法用于发送报文dp到目的地。