实现网络通信,首先必须确定通信双方的对象。因此知道两个元素:IP地址和端口号。通过IP地址和端口号就可以定位到一台确定的主机上。
IP的作用是唯一定位一台网络上的计算机。IP地址的分类方式有两种:
通过IP地址分类(IPV4/IPV6):
通过公网地址和私网地址分类:
在Java中,有一个InetAddress类,专门用于操作IP相关的属性。需要注意的是,该类没有构造方法,其方法都为静态方法,可直接调用。常用方法如下:
getAddress() :返回此 InetAddress 对象的原始 IP 地址。
getByAddress(String host, byte[] addr) :根据提供的主机名和 IP 地址创建 InetAddress。
getByName(String host) :在给定主机名的情况下确定主机的 IP 地址。
//实例:获取本机IP
InetAddress.getByName("localhost");
InetAddress.getByName("127.0.0.1");
getLocalHost() :返回本地主机。
端口表示一个程序的进程,不同的进程有不同的端口号,端口号的范围被定位为0~65535。端口分类如下:
共有端口0~1023 (不建议使用)
程序注册端口:1024~49151
动态、私有端口:49152~65535
常用相关DOS命令:
netstat - ano #查看所有端口
netstat - ano|findstr "5900"//查看指定(5900)的端口
tasklist|findstr "8696" //查看指定端口的进程
假设两台机器上分别存在两个进程:QQ(假设端口号为8570),MSN(假设端口号为9527),机器1上的QQ要给机器2的QQ发消息,那么机器1需要先通过IP地址定位主机2,然后再通过端口号8570找到QQ进程,这样才能进行消息的收发。假如消息发错了,比如发给了端口号为9527的MSN,那么此消息会作废。
Java中也有与端口相关的类InetSocketAddress,该类存在构造方法:
InetSocketAddress(InetAddress addr, int port):根据 IP 地址和端口号创建套接字地址
InetSocketAddress(int port) :创建套接字地址,其中 IP 地址为通配符地址,端口号为指定值。
InetSocketAddress(String hostname, int port) :根据主机名和端口号创建套接字地址。
常用方法如下:
getAddress() :获取 InetAddress。
getHostName():获取 hostname。
getPort():获取端口号。
客户端步骤:
public class TCPClientDemo01 {
public static void main(String[] args) {
Socket socket = null;
OutputStream outputStream = null;
try {
InetAddress localHost = InetAddress.getByName("127.0.0.1");
int port = 9999;
socket = new Socket(localHost, port);
outputStream = socket.getOutputStream();
outputStream.write("老八秘制小汉堡".getBytes());
} catch (Exception e) {
e.printStackTrace();
}finally {
if (outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务器:
public class TCPServerDemo01 {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream inputStream = null;
ByteArrayOutputStream bos = null;
try {
serverSocket = new ServerSocket(9999);
while (true) {
socket = serverSocket.accept();
inputStream = socket.getInputStream();
//利用内存操作流的缓冲器特性,防止传输中文时出现乱码
bos = new ByteArrayOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) != -1) {
bos.write(bytes);
}
System.out.println(bos.toString());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//socket也需要进行关闭,并且遵循一个原则:先开后关
if (bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意:**编译时一定要先运行服务端,再运行客户端,否则会报java.net.ConnectException: Connection refused: connect异常。**这是初学时一个比较容易犯的问题。
文件上传
大致步骤同上,但是需要注意传输什么类型的数据就需要使用什么类型的IO流。
客户端:
public class TCPClientDemo02 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9527);
OutputStream outputStream = socket.getOutputStream();
//因为处理的是文件,因此需要字节流来读取数据
FileInputStream fis = new FileInputStream("student.txt");
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes))!= -1){
outputStream.write(bytes);
}
/*写完数据后必须要告诉服务端,否则会陷入死循环:
客户端发送完数据后一直再等待服务端发送接收完毕信号,
而服务端则一直再等在发送端发送下一个文件
*/
socket.shutdownOutput();
//确定服务端接收完毕才断开连接
InputStream inputStream = socket.getInputStream();
//接收到的是byte类型的数据,因此用内存操作流来接收
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len2 = 0;
while ((len2 = inputStream.read(buffer))!= -1){
byteArrayOutputStream.write(buffer,0,len2);
}
System.out.println(byteArrayOutputStream.toString());
fis.close();
outputStream.close();
socket.close();
inputStream.close();
byteArrayOutputStream.close();
}
}
服务端:
public class TCPServerDemo02 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9527);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
//输出文件需要字节流来输出
FileOutputStream fos = new FileOutputStream("Student2.txt");
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer))!= -1){
fos.write(buffer,0,len);
}
//通知客户端接受完毕
OutputStream outputStream = socket.getOutputStream();
outputStream.write("接收完毕".getBytes());
fos.close();
inputStream.close();
socket.close();
serverSocket.close();
outputStream.close();
}
}
UDP传输不需要连接服务器,只需要知道发送和接收端口就可以进行数据传输。UDP其实没有发送端接收端之分,服务器 端可以是客户端,客户端也可以是服务端。
实现数据传输:
//客户端
public class UDPClientDemo01 {
public static void main(String[] args) throws Exception {
//1.创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket(9527);
//2.将要发送的数据封包
byte[] bytes = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length,InetAddress.getByName("127.0.0.1"), 8578);
//3.发送数据包
socket.send(packet);
socket.close();
}
}
//服务端
public class UDPServerDemo01 {
public static void main(String[] args) throws Exception {
//1.创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket(8578);
//2.将接收到的数据存入接收包
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length);
//接收数据包
socket.receive(packet);
System.out.println(new String(packet.getData()));
socket.close();
}
}
多线程聊天实现:
//发送方
public class SendMSG implements Runnable {
DatagramSocket socket = null;
BufferedReader bR = null;
DatagramPacket packet = null;
//自己端口号
private int fromPort;
//接收方的端口号
private int recievePort;
//接收方的IP地址
private String recieveIP;
//构造函数
public SendMSG(int fromPort, int recievePort, String recieveIP) {
this.fromPort = fromPort;
this.recievePort = recievePort;
this.recieveIP = recieveIP;
try {
socket = new DatagramSocket(fromPort);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
//读取一句话,因为高效字符流有readline方法,故使用
//因为要从键盘读取,因此使用字符流
bR = new BufferedReader(new InputStreamReader(System.in));
while (true) {
//读取输入的话
String s = bR.readLine();
//要封包必须是字节类型,因此转换称字节数组形式
byte[] bytes = s.getBytes();
packet = new DatagramPacket(bytes, 0, bytes.length, new InetSocketAddress(this.recieveIP, this.recievePort));
//发送数据
socket.send(packet);
//收到bye则结束聊天
if (s.equals("bye")) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bR != null) {
try {
bR.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket.close();
}
}
}
//接收方
public class RecieveMSG implements Runnable {
//本地端口号
private int fromPort;
//消息的发送者
private String msgFrom;
DatagramSocket socket = null;
public RecieveMSG(int fromPort,String msgFrom) {
this.fromPort = fromPort;
this.msgFrom = msgFrom;
try {
socket = new DatagramSocket(fromPort);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true) {
//该字节数组1个人理解是为了满足DatagramPacket构造函数而创建
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length);
//接收包
socket.receive(packet);
//将包中的数据存入新的字节数组2
byte[] reciver = packet.getData();
//转为字符串形式输出
String s = new String(reciver, 0, reciver.length);
System.out.println(msgFrom + ":" + s);
//收到bye则退出聊天
if (s.trim().equals("bye")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
socket.close();
}
}
}
以学生和老师为例,主函数如下:
//学生窗口
public class TalkStudent {
public static void main(String[] args) {
new Thread(new SendMSG(9527,8888,"localhost")).start();
new Thread(new RecieveMSG(9999,"老师")).start();
}
}
//老师窗口
public class TalkTeacher {
public static void main(String[] args) {
new Thread(new SendMSG(8570,9999,"localhost")).start();
new Thread(new RecieveMSG(8888,"学生")).start();
}
}
需要注意的是填写的端口:对于发送方,不能直接使用接收方的端口号作为信息的接收者,否则会出现端口占用异常,因此需要一个间接端口(如8888)来发送。
学生发送给老师消息的流程为:学生(端口号9527)将数据发送给端口8888,老师(端口号8570)从端口8888接收消息。