关于java中使用UDP做关于聊天的练习

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34629467/article/details/79394307

    真的没发现,写的挺烂的却也有人能看到,我有一种被监督的感觉!

    我想把这个当成自己的笔记,可以随时翻阅!希望学到的东西就是学到了,并记下了,而不是过一段时间就忘记了!

   今天我学到技术点:

  ①Swing布局: BorderLayout、FlowLayout

  ②Swing界面: 本质就是容器,然后我们在这个平面上面铺面板,好比如拼图一样,只是拼图永远是一层,而JFrame这个是可以一层覆盖一层,最后仍然以平面显示给我们看的!

 ③监听器: 

        键盘监听: 只需要实现 KeyListener

        按钮监听: 只需要实现ActionListerner

        具体: ActionListener,KeyListener

        注意点: 因为组件(组成界面的元件)是一个组件,

        而事件由: 事件源、事件种类、监听组成,

        按钮比作事件源,

        那么按钮按下就是事件,按钮释放也是事件

        那么监听器就是ActionListener,监听打了按下采取一个操作,监听到了释放也采取一个操作:

其实这里还涉及到java的设计模式混编,观察者 + 中介者模式==》实现触发器事件(我这仅仅是联想),因为监听在一定程度上我就理解为事件触发器类

④DataGramSocket: 本质就是一个套接字类,并且是final的,用于在至少两个程序进行通信建立连接,就是说客户端与服务端都需要创建这个套接字对象;

DatagramSocket()
DatagramSocket(int port)
DatagramSocket(int port, InetAddress Iaddr)
port:指明通信所用的端口号,如果未指明端口号,则把socket连接到本地主机上一个可用的端口,给出的端口不能冲突
Iaddr:指明一个可用的本地地址


数据包:DataGramPacket:注意: UDP协议本就是不可靠的、无序的、可重复的通信协议,但是通信效率高呀!

DatagramPacket(byte[] buf,int offset,InetAddress addr,int port,int offset);
buf:存放数据报数据
length:指明数据报中数据的长度
addr和port指明目的地址

offset:指明了数据报的位移量


======================================================================================


多线程:


网络编程:


IO流操作:


文件操作:


下面我想贴下模仿的代码:当然是有很大的改进空间的!

package cn.hd.udp;


import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Date;


import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;


/**
 * 界面设计:
 * ①两个文本框
 * ②一个发送按钮
 * @author dudu
 * Swing的界面其实: 容器中不断添加元素
 *
 */
public class UDPServerDemo extends JFrame implements ActionListener,KeyListener{

JTextField txtAddr, txtPort, txtPort2, txtSendInfo;//连接文本
JTextArea  txtReceInfo;//内容区域
JButton btnConn, btnSend;//连接按钮,发送按钮

/**
* 流布局:
*/
public UDPServerDemo() {
super("UDP聊天丑界面");
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel lbladdr = new JLabel("IP地址", JLabel.RIGHT);
JLabel lblport = new JLabel("通讯端口", JLabel.RIGHT);
txtAddr = new JTextField("127.0.0.1", 14);
txtPort = new JTextField("8888", 5);
txtPort2 = new JTextField("9999", 5);
btnConn = new JButton("连接");
//加入到panel里
topPanel.add(lbladdr);//先右边标签
topPanel.add(txtAddr);//后左边文本
topPanel.add(lblport);//先右边标签
topPanel.add(txtPort);//后左边文本
topPanel.add(txtPort2);//后左边文本
topPanel.add(btnConn);//加入按钮
//将面板加入到容器中: 边界布局靠北
add(topPanel, BorderLayout.NORTH);
txtReceInfo = new JTextArea();
txtReceInfo.setEditable(false);//设置接受框中内容为只读
JScrollPane spane = new JScrollPane(txtReceInfo);
add(spane);

JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
txtSendInfo = new JTextField(35);
btnSend = new JButton("发送(&S)");
btnSend.setMnemonic('S');//快捷键
bottomPanel.add(txtSendInfo);
bottomPanel.add(btnSend);

add(bottomPanel, BorderLayout.SOUTH);
//为按钮设置监听
btnConn.addActionListener(this);
btnSend.addActionListener(this);
txtSendInfo.addKeyListener(this);
//将发送添加回车键监听
// btnSend.addKeyListener(new KeyListener() {
//
// @Override
// public void keyTyped(KeyEvent e) {
//
// }
//
// @Override
// public void keyReleased(KeyEvent e) {
//
// }
//
// @Override
// public void keyPressed(KeyEvent e) {
// if(e.getKeyChar() == KeyEvent.VK_ENTER) {
// System.out.println("true");
// btnSend.doClick();//触发按钮点击事件==》相当于点击了发送,此时监听器监听到这个行为,就将发送内容
// }
// }
// });

setSize(500, 400);
setVisible(true);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

public static void main(String[] args) {
new UDPServerDemo();
}


@Override
public void actionPerformed(ActionEvent e) {
//点击连接按钮时启动接收线程
if(e.getSource() == btnConn) {
int nport = Integer.parseInt(txtPort.getText().trim());
new ReceiveThread(nport).start();//启动线程
btnConn.setEnabled(false);//设置按钮为不可用状态
// JOptionPane.showMessageDialog(null, "连接成功,开始聊吧!");
txtReceInfo.setText("连接成功,开始聊吧!\n");
}
//发送按钮就是发送线程
if(e.getSource() == btnSend) {
// JOptionPane.showMessageDialog(null, "连接成功,开始发送消息吧!");
//先判断是否创建了连接
if(btnConn.isEnabled()) {
JOptionPane.showMessageDialog(null, "还未连接服务器,请先连接...");
txtSendInfo.setText("");
return;
} else {
//可以开始发送UDP数据
//要发送信息给对方法的ip地址:其实就是本机ip
String addr = txtAddr.getText().trim();
int nport2 = Integer.parseInt(txtPort2.getText().trim());
//获取原来的所有接收框的内容
String str = txtReceInfo.getText();
//获取要发送的内容
String msg = txtSendInfo.getText();
//将其转换成字节数组
byte[] bs = msg.getBytes();
//设置ip对方ip
try {
InetAddress iaddr = InetAddress.getByName(addr);
//创建发送的datagramSocket
DatagramSocket socket = new DatagramSocket();
//打包
/*
* 1:bs:要发送的内容
* 2:开始位置:从哪个位置开始发送
* 3:bs.length:发送的长度
* 4:发送的对方ip地址
* 5:通过哪个端口发送: 这个端口由服务器决定
*/
DatagramPacket p = new DatagramPacket(bs, 0, bs.length, iaddr, nport2);
//发送: 将我的内容发送给别人,同时也发送到我自己的对话框中
socket.send(p);
str += "本机Ip:" + iaddr + ":" + nport2 + " " + new Date().toLocaleString() + "\n" + msg + "\n";
txtReceInfo.setText(str);
txtSendInfo.setText("");
socket.close();
} catch (UnknownHostException ex) {
ex.printStackTrace();
} catch (SocketException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}

/**
* 负责来接收消息
* 接收线程:
* @author dudu
*
*/
class ReceiveThread extends Thread {

String addr;
int port;
DatagramSocket receiveSocket = null;
DatagramPacket receiveData = null;

public ReceiveThread(int port) {
// this.addr = addr;
this.port = port;
try {
//启动socket
// receiveSocket = new DatagramSocket(port);
receiveSocket = new DatagramSocket(port);
} catch (SocketException e) {
e.printStackTrace();
}
}


@Override
public void run() {
//然后这就是一个专门接收信息的线程
byte[] buf = new byte[1000];
while(true) {
//创建一个接收数据的包:一次最多64K,接收后放到字节数组中去
receiveData = new DatagramPacket(buf, buf.length);
try {
//接收对方发送的数据
receiveSocket.receive(receiveData);//等待消息(阻塞状态)反正一直会阻塞在这个地方
//接收到流socket中后需要解析
String str = new String(buf, 0, receiveData.getLength());
//获取接收框中原有的内容
String msg = txtReceInfo.getText();
msg += "对方机Ip:" + receiveData.getAddress() + ":" + receiveData.getPort() + " " +  new Date().toLocaleString() + "\n" +  str + "\n";
//把内容显示到接收框中
txtReceInfo.setText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


@Override
public void keyTyped(KeyEvent e) {

}


@Override
public void keyPressed(KeyEvent e) {
if(e.getKeyChar() == KeyEvent.VK_ENTER) {
// System.out.println("true");
btnSend.doClick();//触发按钮点击事件==》相当于点击了发送,此时监听器监听到这个行为,就将发送内容
}
}


@Override
public void keyReleased(KeyEvent e) {

}

}

总结: 其实我就是想记录记录每天学习的东西以及一些心得!

希望能被提出意见!谢谢!




 


展开阅读全文

关于UDP做的聊天程序,求助

08-06

我想问下为什么不能实现指定ip间的聊天,只能本机发送接受信息,我是菜鸟,有劳了。rnimport java.awt.*;rnimport java.awt.event.ActionEvent;rnimport java.awt.event.ActionListener;rnimport java.awt.event.WindowAdapter;rnimport java.awt.event.WindowEvent;rnimport java.io.IOException;rnimport java.net.DatagramPacket;rnimport java.net.DatagramSocket;rnimport java.net.InetAddress;rnimport java.net.SocketException;rnimport java.net.UnknownHostException;rnpublic class Chat extends Frame rn DatagramSocket dsSend = null;rn DatagramSocket dsRecv = null;rn DatagramPacket sendPack = null;rn DatagramPacket recvPack = null;rn TextField tfTextData = new TextField();rn TextField tfTextIP = new TextField();rn List list = new List(6);rn public Chat() throws HeadlessException rn this.launchFrame();rn new Thread(new Recv()).start();rn rn public void launchFrame() rn setLocation(300, 200);rn this.setSize(500, 500);rn this.add(list, "Center");rn Panel panel = new Panel();rn this.add(panel, "South");rn panel.setLayout(new BorderLayout());rn panel.add(tfTextData, "Center");rn panel.add(tfTextIP, "South");rn // pack();rn addWindowListener(new WindowAdapter() rn public void windowClosing(WindowEvent e) rn dsRecv.close();rn System.exit(0);rn rn );rn tfTextData.addActionListener(new TFListener());rn setVisible(true);rn rn class TFListener implements ActionListener rn public void actionPerformed(ActionEvent e) rn String s = tfTextData.getText().trim();rn tfTextData.setText("");rn byte[] buf = s.getBytes();rn try rn dsSend = new DatagramSocket();rn sendPack = new DatagramPacket(buf, 0, buf.length,rn InetAddress.getByName(tfTextIP.getText()), 19999);rn dsSend.send(sendPack);rn catch (SocketException e1) rn e1.printStackTrace();rn catch (UnknownHostException e1) rn e1.printStackTrace();rn catch (IOException e1) rn e1.printStackTrace();rn rn dsSend.close();rn rn rn class Recv implements Runnable rn public void run() rn try rn dsRecv = new DatagramSocket(19999);rn byte[] buf = new byte[1024];rn recvPack = new DatagramPacket(buf, buf.length);rn while (true) rn dsRecv.receive(recvPack);rn // System.out.println(new String(recvPack.getData(), 0,rn // recvPack.getLength()));rn list.add("ip为:"rn + recvPack.getAddress().getHostAddress()rn + " 说: "rn + new String(recvPack.getData(), 0, recvPackrn .getLength())+" 该ip的端口: "+recvPack.getPort());rn rn catch (IOException e1) rn if (dsRecv.isClosed() != true) rn e1.printStackTrace();rn rn rn rn rnrnrnrnrnrnrnrnrnrnrnpublic class TestChat rn public static void main(String[] args) rn Chat c = new Chat();rn rn 论坛

关于java socket多用户聊天

12-21

本人想做一个类似QQ的基于Socket的通信软件,但是遇到问题啊。。。我的软件首先登录成功会倒类似QQ好友的那个界面,然后我想把那个界面同时作为客户端和服务端,当作为服务器接受到连接请求,就自动弹出聊天窗口,当作为客户端连接他人时,也可以与别人聊天,跟QQ差不多,比如1,2,3三个用户互相加了为好友,都登录后可以相互聊天,可是当我这么做的话,只有第一个登录的可以连接聊天,但是关闭窗口后在想聊天就吧行了,另外两个打开,压根就不能聊天,这是什么原因啊。。我把我主要的代码贴上,,首先是登录以后的那个界面rnpublic FriendList(String name, int id, String LoginId, String ip) rn this.name = name;rn this.id = id;rn this.LoginId = LoginId;rn this.ip = ip;rn initComponents();rn BindList();rnthis.thread = new Thread(this);rn this.thread.start();rnrnrnpublic void run() rn try rn while (true) rn ss = new ServerSocket(port);rn System.out.println("正在等待连接.....");rn client = ss.accept();rn new ChatFrame(name, client).setVisible(true);rn port++;rn rn catch (IOException ioe1) rn System.out.println("Can't set up user thread" + ioe1);rn rn rnrnprivate void jButton1ActionPerformed(java.awt.event.ActionEvent evt) rn //String aname = jList1.getSelectedValue().toString();rn try rn new ChatFrame(name, new Socket(ip, port)).setVisible(true);rn catch (UnknownHostException e) rn System.out.println(e);rn e.printStackTrace();rn catch (IOException e) rn System.out.println(e);rn e.printStackTrace();rn rnrn rnrn接着是聊天窗口的那个类得代码rnpublic ChatFrame(String name, Socket socket) rn initComponents();rn this.name = name;rn this.socket = socket;rn this.thread = new Thread(this);rn this.thread.start();rn rn rnpublic void run() rn try rn this.cout = new PrintWriter(socket.getOutputStream(), true);rn this.cout.println(name);rn BufferedReader cin = new BufferedReader(new InputStreamReader(rn socket.getInputStream()));rn name1 = cin.readLine();rn this.setTitle("正在和" + name1 + "聊天");rn String aline = cin.readLine();rn while (aline != null) rn jTextArea2.append(aline + "\r\n"); rn Play("D:\\MyEclipse\\Workspaces\\JAVA_Chat\\Sound\\MSN.wav");rn try rn rn Thread.sleep(1000);rn catch (InterruptedException e) rn // TODO Auto-generated catch blockrn e.printStackTrace();rn rn aline = cin.readLine();rn rn cin.close();rn cout.close();rn socket.close();rnrn catch (IOException e) rn System.out.println(e);rn e.printStackTrace();rn rnrn求大神帮助啊,Socket刚学。。菜鸟啊 论坛

关于JAVAUDP网络程序

07-19

在实现这个问题:主机不断地重复播出节目预报,可以保证加入到同一组的主机随时可接收到广播信息。接收者将正在接收的信息放在一个文本域中,并将接收的全部信息放在另一个文本域中。rn接收程序出错:Exception in thread "AWT-EventQueue-0" java.lang.IllegalThreadStateExceptionrn at java.lang.Thread.start(Unknown Source)rn at Receive.actionPerformed(Receive.java:68)rn at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)rn at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)rn at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)rn at javax.swing.DefaultButtonModel.setPressed(Unknown Source)rn at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)rn at java.awt.Component.processMouseEvent(Unknown Source)rn at javax.swing.JComponent.processMouseEvent(Unknown Source)rn at java.awt.Component.processEvent(Unknown Source)rn at java.awt.Container.processEvent(Unknown Source)rn at java.awt.Component.dispatchEventImpl(Unknown Source)rn at java.awt.Container.dispatchEventImpl(Unknown Source)rn at java.awt.Component.dispatchEvent(Unknown Source)rn at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)rn at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)rn at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)rn at java.awt.Container.dispatchEventImpl(Unknown Source)rn at java.awt.Window.dispatchEventImpl(Unknown Source)rn at java.awt.Component.dispatchEvent(Unknown Source)rn at java.awt.EventQueue.dispatchEvent(Unknown Source)rn at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)rn at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)rn at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)rn at java.awt.EventDispatchThread.pumpEvents(Unknown Source)rn at java.awt.EventDispatchThread.pumpEvents(Unknown Source)rn at java.awt.EventDispatchThread.run(Unknown Source)rnException in thread "AWT-EventQueue-0" java.lang.IllegalThreadStateExceptionrn at java.lang.Thread.start(Unknown Source)rn at Receive.actionPerformed(Receive.java:68)rn at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)rn at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)rn at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)rn at javax.swing.DefaultButtonModel.setPressed(Unknown Source)rn at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)rn at java.awt.Component.processMouseEvent(Unknown Source)rn at javax.swing.JComponent.processMouseEvent(Unknown Source)rn at java.awt.Component.processEvent(Unknown Source)rn at java.awt.Container.processEvent(Unknown Source)rn at java.awt.Component.dispatchEventImpl(Unknown Source)rn at java.awt.Container.dispatchEventImpl(Unknown Source)rn at java.awt.Component.dispatchEvent(Unknown Source)rn at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)rn at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)rn at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)rn at java.awt.Container.dispatchEventImpl(Unknown Source)rn at java.awt.Window.dispatchEventImpl(Unknown Source)rn at java.awt.Component.dispatchEvent(Unknown Source)rn at java.awt.EventQueue.dispatchEvent(Unknown Source)rn at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)rn at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)rn at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)rn at java.awt.EventDispatchThread.pumpEvents(Unknown Source)rn at java.awt.EventDispatchThread.pumpEvents(Unknown Source)rn at java.awt.EventDispatchThread.run(Unknown Source)rn其中广播主机程序代码为:[code=Java][/code]rnimport java.io.IOException; //导入java.io.IOException类rnimport java.net.*; //导入java.net包rnpublic class Weather extends Thread //创建类。该类为多线程执行程序rn String weather = "节目预报:八点有大型晚会,请收听";rn int port = 9898; //定义端口rn InetAddress iaddress = null; //创建InetAddress对象rn MulticastSocket socket = null; //声明多点广播套接字rn Weather() //构造方法rn try rn iaddress = InetAddress.getByName("224.255.10.0"); //实例化InetAddress,指定地址rn socket = new MulticastSocket(port); //实例化多点广播套接字rn socket.setTimeToLive(1); //指定发送范围是本地网络rn socket.joinGroup(iaddress); //加入广播组rn catch (Exception e) rn e.printStackTrace(); //输出异常信息rn rn rn public void run() //run()方法rn while(true)rn DatagramPacket packet = null; //声明DatagramPacket对象rn byte data[] = weather.getBytes(); //声明字节数组rn packet = new DatagramPacket(data,data.length,iaddress,port); //将数据打包rn System.out.println(new String(data)); //将广播信息输出rn try rn socket.send(packet); //发送数据rn sleep(3000); //线程休眠rn catch (Exception e) rn e.printStackTrace(); //输出异常信息rn rn rn rn public static void main(String[] args) // 主方法rn Weather w = new Weather(); //创建本类对象rn w.start(); //启动线程rn rnrn接收代码为:[code=Java][/code]rnimport java.awt.event.*; //导入java.awt.event包rnimport javax.swing.*;rnimport java.awt.*;rnimport java.net.*;rnpublic class Receive extends JFrame implements Runnable, ActionListener rn int port; //定义int型变量rn InetAddress group = null; //声明InetAddress对象rn MulticastSocket socket = null; //创建多点广播套接字对象rn JButton ince = new JButton("开始接收"); //创建按钮对象rn JButton stop = new JButton("停止接收");rn JTextArea inceAr = new JTextArea(10, 10); //显示接收广播的文本域rn JTextArea inced = new JTextArea(10, 10);rn Thread thread; //创建Thread对象rn boolean b = false; //创建boolean型变量rn public Receive() //构造方法rn super("广播数据报"); //调用父类方法rn thread = new Thread(this);rn ince.addActionListener(this); //绑定按钮ince的单击事件rn stop.addActionListener(this); //绑定按钮stop的单击事件rn inceAr.setForeground(Color.blue); //指定文本域中文字颜色rn JPanel north = new JPanel(); //创建Jpane对象rn north.add(ince); //将按钮添加到面板north上rn north.add(stop);rn add(north, BorderLayout.NORTH); //将north放置在窗体的上部rn JPanel center = new JPanel(); //创建面板对象centerrn center.setLayout(new GridLayout(1, 2)); //设置面板布局rn center.add(inceAr); //将文本域添加到面板上rn center.add(inced);rn add(center, BorderLayout.CENTER); //设置面板布局rn validate(); //刷新rn port = 9898; //设置端口号rn try rn group = InetAddress.getByName("224.255.10.0"); //指定接收地址rn socket = new MulticastSocket(port); //绑定多点广播套接字rn socket.joinGroup(group); //加入广播组rn catch (Exception e) rn e.printStackTrace(); //输出异常信息rn rn setBounds(100, 50, 360, 380); //设置布局rn setVisible(true); //将窗体设置为显示状态rn rn public void run() //run()方法rn while (true) rn byte data[] = new byte[1024]; //创建byte数组rn DatagramPacket packet = null; //创建DatagramPacket对象rn packet = new DatagramPacket(data, data.length, group, port); //待接收的数据包rn try rn socket.receive(packet); //接收数据包rn String message = new String(packet.getData(), 0, packetrn .getLength()); //获取数据包中内容rn inceAr.setText("正在接收的内容:\n" + message); //将接收内容显示在文本域中rn inced.append(message + "\n"); //每条信息为一行rn catch (Exception e) rn e.printStackTrace(); //输出异常信息rn rn if (b == true) //当变量等于true时,退出循环rn break;rn rn rn rn public void actionPerformed(ActionEvent e) //单击事件rn if (e.getSource() == ince) //单击按钮ince触发的事件rn ince.setBackground(Color.red); //设置按钮颜色rn stop.setBackground(Color.yellow);rn if (!(thread.isAlive())) //如线程不处于“新建状态”rn thread = new Thread(this); //实例化Thread对象rn rn thread.start(); //启动线程rn b = false; //设置变量值rn rn if (e.getSource() == stop) //单击按钮stop触发的事件rn ince.setBackground(Color.yellow); //设置按钮颜色rn stop.setBackground(Color.red);rn b = true; //设置变量值srn rn rn public static void main(String[] args) //主方法rn Receive rec = new Receive(); //创建本类对象rn rec.setSize(460, 200); //设置窗体大下rn rnrn 论坛

没有更多推荐了,返回首页