<div
class="iteye-blog-content-contain" style="font-size: 14px;">
<br><br>?<br><br>? 所谓网络通信聊天室,简单的说就是用代码实现一个以本机ip作为地址的服务器,并能够实现与客户端的
数据传递。服务器从客户端的输入流中读取数据,在客户端的输出流中写入数据,从而达到通信目的。
?
[size=18px;]
一、服务器的建立[/size]
1.首先,建立一个服务器。
Java中关于网络通信的类在java.net包中,建立服务器需要调用到ServerSocket类,建立服务器也就是new一个ServerSocket:
ServerSocket ss = new ServerSocket(8888);
?其中8888表示要建立的服务器的端口号(每台计算机可用的端口号为0~65535,0~1024的端口号为“知名端口”应尽量避免使用),客户机通过该端口号来连上服务器。
?
2.然后,等待客户机的连入。
Socket socket = ss.accept();
???????????? 服务器建立成功后,应保持等待的状态等待客户机的连入,ss.accept()表示了这一等待过程,在有客户端连入之前程序会阻塞在这一步,直到有客户机访问后返回一个Socket类的对象。
?
3.接着,处理服务器接收到的Socket类的对象。
????????????? 对于获取到的客户机对象,服务器如果要实现通信,就要从该对象上获取输入输出流,并进行相应的读写操作,从输入流中读取数据,在输出流中写入数据,实现通信。例如:
try {
???
???
??? InputStream input = socket.getInputStream();
???
OutputStream output = socket.getOutputStream();
???
???
//向对方发送一个字符串
???
String str = "欢迎连接!";
byte[] b = str.getBytes();
output.write(b);
//从网络输入流中读取字符
???
int c = input.read();
System.out.print(c);
} catch (IOException e) {
???
???
e.printStackTrace();
??
}
?
当然,?这几行代码只实现了最简单的读写,给客户端发了一句话,从客户端读取了一个字符后服务器就关闭了。如果要不停地从客户机读取数据,可以将读取操作放在
循环中进行。而且还要注意到读写数据都是以字节为单位进行的,在实际操作中人们不可能直接操作字节进行聊天,必须进行和字符串的相互转化。
?????? 写操作时的转化较为简单,直接“字符串”.getBytes()就可以转化成字节,在write()之后加一个flush()语句,让写的数据强制输出,防止读操作完成而写操作未完成时服务器close()使数据丢失。
??????从输入流中读数据时,为使代码清晰,定义一个readline()方法,并把输入流作为形参传入该方法。对于输入流中的
数据处理方式可以有多种,胡哥讲的是建立StringBuffer即字符串缓冲区,把读到的每一个数据append()进去,然后转化成String;之前强哥讲的方法是建立一个动态数组流ByteArrayOutputStream()读完后转化为字节数组,再转化为字符串,具体操作在如下代码中。
?
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {
public void setup(){
try {
//建立服务器
ServerSocket ss = new ServerSocket(8888);
System.out.println("服务器建立成功");
Socket socket = ss.accept();//阻塞,等待客户机连入
//获取输入输出流
InputStream ins = socket.getInputStream();
OutputStream ous = socket.getOutputStream();
String str = "欢迎连接!";
byte[] b = str.getBytes("GB2312");//转码,“GB2312”为汉字
ous.write(b);
ous.flush();//强制输出
String s = readline(ins);
while(!s.equals("exit")){//如果读到的字符串不是"exit”就退出,否则继续读
System.out.println("读到的数据是:"+s);
s = readline(ins);
}
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//定义一个读取一行字符串的方法
public String readline(InputStream input) throws IOException{
//动态数组流,相当于一个动态字节数组,或者一个字节队列
ByteArrayOutputStream bytearrayous = new ByteArrayOutputStream();
int c = input.read();
while(c!='\n'){
if(c!='\r')
bytearrayous.write(c);
c = input.read();
}
byte [] b = bytearrayous.toByteArray();//转换为字节数组
String s = new String(b,"GB2312");//将字节数组转换为字符串
//System.out.println("读到的数据是:"+s);
return s;
}
public static void main(String[] args) {
MyServer server = new MyServer();
server.setup();
}
}
?
另外,有IO操作时要注意try{}catch{}或者抛出
异常。
4.最后,将服务器与该客户机的连接关闭。
socket.close();
?
?
[size=18px;]
二、用cmd命令连入服务器[/size]
???????? 每一台电脑本身既可以做服务器也可以做客户机,用自己的电脑作为客户端连入这个服务器进行简单的测试,telnet命令是联网操作,输入方式为“telnet ip地址 宽口号”。
??????? 打开cmd,如下图:
<img src="/Upload/Images/2014032206/0534A7C9B2C48E21.jpg" alt="" width="219" height="163">
因为要连入服务器,所以需要知道服务器也就是本机的ip地址(端口号已知为8888),可以直接输入ipconfig查询本机地址,也可以用localhost直接表示本机地址。
在命令行窗口中输入telnet localhost 8888,点enter确定,弹出如下窗口:
<img src="/Upload/Images/2014032206/D2FBDFCDF14BF250.jpg" alt="" width="231" height="140">
证明连接成功,输入exit就退出,界面上显示遗失对主机的连接。
可是测试时
发现如果不输入exit直接关闭cmd界面程序会报错,仔细查找后发现强退时服务器会在循环里不停地读到-1,程序进入死循环。可以在读到-1时让readline函数自动返回"exit”解决这一问题。
?
?
[size=18px;]
三、实现简单群聊[/size]
1.让多个客户机连入服务器
之前的代码简单的实现了让客户机连入服务器,不过由于服务器accept()每次只能接入一个客户机,而且该客户机退出后服务器也自动关闭了,在实际生活中肯定没有只接待一个客户机就关闭的服务器,所以要让服务器一直开着可以在accept()周围添加一个while(true)语句(虽然直接把true放到判断里不好这里这样做也没什么大的影响)。因为不同的客户机是完全独立的,所以要把对客户机连接的处理放到
线程里进行,这样多个客户端可以同时
在线。
建立一个继承自Thread的SocketThread类,注意把socket对象传入处理线程,之前读写操作的处理放到processSocket()中:
public class SocketThread extends Thread{
private Socket socket;
public SocketThread(Socket socket){
this.socket = socket;
}
public void run(){
processSocket(socket);
}
public void processSocket(Socket socket){}
}
?
在accept()获取到socket对象后就启动该线程:
while(true){
Socket socket = ss.accept();
SocketThread socketthread = new SocketThread(socket);
socketthread.start();
System.out.println("启动了一个线程");
}
?
2.让客户机群聊
服务器面对一个客户机时可以从该客户机的输入流中读取数据,向输出流中写入数据;如果多个客户机在线时服务器可以把每个客户机输入流中读到的数据写给每个输出流,这样就实现了群聊或者消息的广播式推送,如果只对某个客户机发送就实现私聊了。
?????? 建立一个队列,在每次有客户机连入时将该客户机加入队列,并在读取到某个客户机发来的消息后,将该消息发给该队列里除自己外的所有成员。为了提高代码复用率,将之前写好的往输出流中进行写操作的代码提出来放到一个sendMsg()的方法里,这样发消息时就可以直接调用该方法。
public void sendMsg(String s) throws IOException{
String st = s+"\n\r";
byte[] b = st.getBytes();
ous.write(b);
ous.flush();
}
?建立队列如下:
public static List<SocketThread> socketlist = new ArrayList();
? 在获取到Socket对象时把该对象的线程加入队列:
while(true){
Socket socket = ss.accept();
SocketThread socketthread = new SocketThread(socket,socketlist);
socketlist.add(socketthread);
socketthread.start();
System.out.println("启动了一个线程");
}
?注意修改SocketThread类的
构造器,把队列传进去。
把得到的
消息发送给除了自己的每个人:
while(!s.equals("exit")){
System.out.println("读到的数据是:"+s);
for(int i=0;i<socketlist.size();i++){
SocketThread so = socketlist.get(i);
if(so!=this)
so.sendMsg(s);
}
s = readline(ins);
}
?
要注意在某个客户机退出聊天后要将该线程从队列中remove掉。
?
?
为了方便分清楚以及实际情况考虑,需要再建立一个User类,保存用户的姓名和密码,这样在群聊时就可以看见说话人的名字。
定义一个login()方法用来获取用户名和密码:
public void login(InputStream ins) throws Exception{
String str = "欢迎连接!";
sendMsg(str);
str ="请输入用户名:";
sendMsg(str);
String name = readline(ins);
if(user.getName().equals("exit"))
return;
str ="请输入密码:";
sendMsg(str);
String password = readline(ins);
user.setPassword(password);
System.out.println("得到的用户密码为:"+user.getPassword());
if(user.getPassword().equals("exit"))
return;
str="登陆成功!";
sendMsg(str);
}
?群发消息时可以加上用户的名字了:
for(int i=0;i<socketlist.size();i++){
SocketThread so = socketlist.get(i);
if(so!=this)
so.sendMsg(user.getName()+"说:"+s);
}
?
?
?
[size=18px;]
四、为服务器添加界面[/size]
给服务器添加一个界面可以方便管理客户机,直接通过界面上操作对客户机实行群发消息、踢人等操作。
因为accept()获取socket对象时是死循环所以不能把建立界面放在阻塞操作后边,当然最好的方式是把这个循环放到线程里单独执行,再建一个线程等待客户机接入。
下面的代码简单的实现了把服务器界面上的消息群发给客户机的功能(给发送按钮添加
监听器,按下按钮时提取文本输入框内的字符串群发给客户机):
public class ServerFrame {
public static List<SocketThread> socketlist = new ArrayList();
public void init (){
JFrame serverframe = new JFrame();
serverframe.setTitle("服务器");
serverframe.setSize(500, 400);
serverframe.setLocationRelativeTo(null);
serverframe.setDefaultCloseOperation(3);
JPanel northPanel = createNorthPanel();
serverframe.add(northPanel,BorderLayout.NORTH);
serverframe.setVisible(true);
}
public JPanel createNorthPanel(){
JPanel jp = new JPanel();
final JTextField sendtext = new JTextField();
sendtext.setPreferredSize(new Dimension(300, 30));
JButton send = new JButton("发送");
send.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
for(int i=0;i<socketlist.size();i++){
SocketThread socketthread = socketlist.get(i);
try {
socketthread.sendMsg(sendtext.getText());
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
});
jp.add(sendtext);
jp.add(send);
return jp;
}
public JPanel createCenterPanel(){
JPanel jp = new JPanel();
return jp;
}
public void initserver(){
ServerThread serverthread = new ServerThread(socketlist);
serverthread.start();
}
public static void main(String[] args) {
ServerFrame sf = new ServerFrame();
sf.initserver();
sf.init();
}
}
?<br><img src="/Upload/Images/2014032206/F2AB97C8D8DAA0C7.jpg" alt="" width="266" height="221"><br>?
?
[size=18px;]
五、为客户机添加界面[/size]
前边的操作客户机都是用cmd里telnet来连接服务器的,在实际使用时这样一个原始的操作界面肯定不会让人满意的,所以要让客户机在UI界面上操作。
1.建立客户机
客户机是独立的,最好放到另一个包下或者再建一个工程。
客户机的实例化和服务器类似,不过客户机的构造器多了一个String 的ip 参数:
Socket client = new Socket("localhost",8888);
?Socket(String ip地址,int 端口号);
然后也是获取输入输出流并进行读写操作,只不过客户机从服务器的输入流中读取数据,向服务器的输出流中写入数据。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class myClient {
private OutputStream ous;
public void setup(){
try{
Socket client = new Socket("localhost",8888);
InputStream ins = client.getInputStream();
ous = client.getOutputStream();
System.out.println("客户机建立成功");
String str = readLine(ins);
while(true){
System.out.println("服务器发来的消息是:"+str);
str = readLine(ins);
}
}catch(Exception e){
e.printStackTrace();
}
}
public String readLine(InputStream in) throws IOException{
ByteArrayOutputStream btayous = new ByteArrayOutputStream();
int c = in.read();
while(c!='\n'){
if(c!='\r')
btayous.write(c);
c = in.read();
}
byte [] b = btayous.toByteArray();
String s = new String(b,"GB2312");
return s;
}
public static void main(String[] args) {
myClient ct = new myClient();
ct.setup();
}
}
?2.添加界面
新建一个类用来实例化界面,把读写操作、对输入输出流的处理放到线程内,基本操作和给服务器添加界面还是一样的。
因为从服务器发来的消息要显示到界面上,所以特别建立了一个MsgListener()的
接口,每次服务器发来新的内容时就会触发监听器,将新的字符串输出到界面聊天窗口上。
public interface MsgListener {
public void onMsg(String msg);
}
?
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientThread extends Thread{
private MsgListener listener;
private OutputStream ous;
private String ip;
private int port;
private String Msg;
public ClientThread(String ip,int port){
this.ip = ip;
this.port = port;
}
//定义一个setListener方法
public void setListener(MsgListener listener) {
this.listener = listener;
}
public void run(){
setup();
}
public void setup(){
try{
Socket client = new Socket(ip,port);
//获取输入输出流
InputStream ins = client.getInputStream();
ous = client.getOutputStream();
System.out.println("客户机建立成功");
Msg = readLine(ins);
while(true){
if(listener != null){//如果不为空刷新listener里的Msg
listener.onMsg(Msg);
}
System.out.println("服务器发来的消息是:"+Msg);
Msg = readLine(ins);
}
}catch(Exception e){
e.printStackTrace();
}
}
private String readLine(InputStream in) throws IOException{
//相当于动态字节数组
ByteArrayOutputStream btayous = new ByteArrayOutputStream();
int c = in.read();
while(c!='\n'){
if(c!='\r')
btayous.write(c);
c = in.read();
}
byte [] b = btayous.toByteArray();
String s = new String(b,"GB2312");//"GB2312"汉字
return s;
}
//发送消息的方法
public void sendMsg(String s){
try{
String st = s+"\n\r";
byte[] b = st.getBytes();
ous.write(b);
ous.flush();//强制输出
}catch(Exception e){
e.printStackTrace();
}
}
}
?
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class ClientFrame {
private ClientThread mc;
private JTextArea jtreceive;
private JTextField jtextclsd;
public void init(){
JFrame jf = new JFrame();
jf.setTitle("客户机");
jf.setSize(500, 400);
jf.setDefaultCloseOperation(3);
jf.setLocationRelativeTo(null);
jf.setResizable(false);
JPanel northPanel = createNorthPanel();
JPanel centerPanel = createCenterPanel();
jf.add(northPanel,BorderLayout.NORTH);
jf.add(centerPanel,BorderLayout.CENTER);
jf.setVisible(true);
}
public JPanel createCenterPanel(){
JPanel jp = new JPanel();
jtextclsd = new JTextField();
jtextclsd.setPreferredSize(new Dimension(350,35));
JButton jbclsd = new JButton("发送");
jbclsd.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
mc.sendMsg(jtextclsd.getText());
jtreceive.append("我说:"+jtextclsd.getText()+"\n");//将客户机自己发送的内容显示到聊天界面上
jtextclsd.setText("");
}
});
jp.add(jtextclsd);
jp.add(jbclsd);
return jp;
}
public JPanel createNorthPanel(){
JPanel jp = new JPanel();
jtreceive = new JTextArea();
jtreceive.setPreferredSize(new Dimension(400,300));
jtreceive.setEditable(false);//设置不可编辑
jp.add(jtreceive);
return jp;
}
//每收到新的消息就追加到界面上
MsgListener l = new MsgListener(){
public void onMsg(String msg) {
System.out.println("收到消息:"+msg);
jtreceive.append(msg+"\n\r");
}
};
public void createClient(){
mc = new ClientThread("localhost",8888);
mc.start();
mc.setListener(l);
}
public static void main(String[] args) {
ClientFrame cf = new ClientFrame();
cf.init();
cf.createClient();
}
}
?
实际效果:<br><img src="/Upload/Images/2014032206/108203311210BF79.jpg" alt=""><br>?
</div>
- 大小: 30.8 KB
- 大小: 21.8 KB
- 大小: 15.3 KB
- 大小: 44.7 KB