目录
一 介绍
读者掌握多线程,io流,网络编程基础知识即可看懂代码以及自制,十分易懂,简单易学,个别代码不懂的也可在网上找资料,后面会附上源代码,希望你们都有收获
二 编写源代码
2.1 创建服务端和客户端
通信肯定至少要2台服务器的,这里为了方便测试就建立2个项目,然后导出去方便观察,读者可以直接建立2个包,一样可以实现,甚至可以在我的基础上再增加或者修改一些功能
public class MyServer {
//服务端
public static void main(String[] args) throws Exception {//抛出异常
//创建侦听端口
ServerSocket ss=new ServerSocket(8998);//端口要一致且不能跟电脑已有端口重复
//调用侦听端口对象的accept方法接收客户端的连接请求
Socket socket;//下面应该要接收一个accept方法接收客户端信息的,我放在下面
public class MyClient {
//客户端
public static void main(String[] args) throws Exception {
//创建一个socket对象,请求连接远程服务器
Socket socket=new Socket("localhost",8998);//本机及端口
TCP三次握手过程
1 主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B ,向主机B 请求建立连接,通过这个数据段, 主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.
//向客户端发送消息
boolean flag=true;
Scanner input=new Scanner(System.in);
while(true) {
while(flag) {
System.out.println("协议:群聊前缀“all:”私聊“人名:”,登录“login:”");
flag=false;
}
System.out.println("请输入消息:");
String message=input.next();
byte[] buffer=message.getBytes();
//使用输出流发送消息给服务器
OutputStream os=socket.getOutputStream();
os.write(buffer);
2 主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事: 我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我
while(true){
System.out.println("准备接收客户端的连接");
socket=ss.accept();//负责和连接的客户通信
System.out.println("有一个客户端连接过来:"+socket.getRemoteSocketAddress());
//创建线程,能同时接收端口请求和接收消息
ServerRecive sr=new ServerRecive(socket);
sr.start();
}
3 主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了 这样3次握手就完成了,主机A和主机B 就可以传输数据了.
//创建一个线程接收服务器回复
ClientReceiver cr=new ClientReceiver(socket);
cr.start();
我这里只用了一个线程,你们也可以考虑用2个线程做一下
2.3 创建功能
聊天一般分为群聊和私聊,所以要有这2个功能,但是怎么区分群聊和私聊呢?QQ里是@全体成员和@个人昵称来区分的,我们也可以定一个类似的协议,然后做一个功能得到区分他们的办法,
这里做了个没有密码的登录功能
while(true) {
//判断当前用户是否登录
String address=socket.getRemoteSocketAddress().toString();
InputStream is = socket.getInputStream();
byte[] buffer=new byte[1024];
int r=is.read(buffer);
String mess=new String(buffer,0,r);
int i=0;
if(mess.startsWith("login:")) {
UserManager.login(address, new Customer(mess.split(":")[1], socket));
OutputStream os=socket.getOutputStream();
byte[] reply="登录成功".getBytes();
os.write(reply);
else {
OutputStream os=socket.getOutputStream();
byte[] reply="请先登录".getBytes();
os.write(reply);
}}
//将登录信息记录到map集合中
public static void login(String address, Customer customer) {
users.put(address, customer);
}
然后就是区分群聊跟私聊了
if(UserManager.checkLogin(address)) {
String[] s=mess.split(":");//mess里面存储了客户端传过来的信息
//根据消息
//判断群聊还是私发
if(s[0].equals( "all")) {//意思就是说前缀是all:
Collection<Customer> users=UserManager.getAllCustomer();//得到所有人的信息,创了个类,里面存储了用户名和地址
System.out.println("现在有"+users.size()+"人在线");
for (Customer customer : users) {//遍历,把自己说的话传给每一个人
if(!(customer.getUsername().equals(UserManager.getUsername(address).getUsername()))) {
OutputStream os=customer.getSocket().getOutputStream();
byte[] messages=(UserManager.getUsername(address).getUsername()+
"说:"+s[1]).getBytes();
os.write(messages);
}//忽略自己
}
}else {
//根据消息要转发给的用户名从map集合获取用户对应的socket
Socket target=UserManager.getSocketByUsername(s[0]);
OutputStream os=target.getOutputStream();
byte[] messages=(UserManager.getUsername(address).getUsername()+
"对你说:"+s[1]).getBytes();
os.write(messages);
}
三 效果图
四 总结
简单吧,一下子就弄完了,代码就是个不断写的过程,希望读者可以多写几遍,加深理解,光copy没用的,这里面要注意的细节太多了,一个不小心就是一连串的bug。比如测试时中间要用英文符号
五 源代码
package qqserver;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class ServerRecive extends Thread {
//服务器处理线程
private Socket socket;
public ServerRecive(Socket socket) {
super();
this.socket = socket;
}
@Override
public void run() {
//接收客户端发来的消息
try {
while(true) {
//判断当前用户是否登录
String address=socket.getRemoteSocketAddress().toString();
InputStream is = socket.getInputStream();
byte[] buffer=new byte[1024];
int r=is.read(buffer);
String mess=new String(buffer,0,r);
int i=0;
if(mess.startsWith("login:")) {
UserManager.login(address, new Customer(mess.split(":")[1], socket));
OutputStream os=socket.getOutputStream();
byte[] reply="登录成功".getBytes();
os.write(reply);
}else {
if(UserManager.checkLogin(address)) {
String[] s=mess.split(":");
//根据消息
//判断群聊还是私发
if(s[0].equals( "all")) {
Collection<Customer> users=UserManager.getAllCustomer();
System.out.println("现在有"+users.size()+"人在线");
for (Customer customer : users) {
if(!(customer.getUsername().equals(UserManager.getUsername(address).getUsername()))) {
OutputStream os=customer.getSocket().getOutputStream();
byte[] messages=(UserManager.getUsername(address).getUsername()+
"说:"+s[1]).getBytes();
os.write(messages);
}
}
}else {
//根据消息要转发给的用户名从map集合获取用户对应的socket
Socket target=UserManager.getSocketByUsername(s[0]);
OutputStream os=target.getOutputStream();
byte[] messages=(UserManager.getUsername(address).getUsername()+
"对你说:"+s[1]).getBytes();
os.write(messages);
}
//System.out.println(UserManager.getUsername(address)+"说"+mess);
}else {
OutputStream os=socket.getOutputStream();
byte[] reply="请先登录".getBytes();
os.write(reply);
}}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class UserManager{
//保存所有已经登录的用户信息
private static Map<String, Customer> users=new HashMap<String, Customer>();
//将登录信息记录到map集合中
public static void login(String address, Customer customer) {
users.put(address, customer);
}
//判断用户是否登录
public static boolean checkLogin(String address) {
return users.containsKey(address);
}
//根据address返回用户名
public static Customer getUsername(String address) {
return users.get(address);
}
//根据用户名获取对应的socket
public static Socket getSocketByUsername(String username) {
Collection<Customer> customers=users.values() ;
Socket target =null;
for (Customer customer : customers) {
if(customer.getUsername().equals(username)) {
target=customer.getSocket();
}
}
return target;
}
//返回所有已经登录的用户
public static Collection<Customer> getAllCustomer() {
return users.values();
}
}
package qqserver;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import sun.applet.Main;
public class MyServer {
public static void main(String[] args) throws Exception {
//创建侦听端口
ServerSocket ss=new ServerSocket(8998);
//调用侦听端口对象的accept方法接收客户端的连接请求
Socket socket;
while(true){
System.out.println("准备接收客户端的连接");
socket=ss.accept();//负责和连接的客户通信
System.out.println("有一个客户端连接过来:"+socket.getRemoteSocketAddress());
//创建线程,能同时接收端口请求和接收消息
ServerRecive sr=new ServerRecive(socket);
sr.start();
}
}
}
package qqclient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class MyClient {
public static void main(String[] args) throws Exception {
//创建一个socket对象,请求连接远程服务器
Socket socket=new Socket("localhost",8998);
//创建一个线程接收服务器回复
ClientReceiver cr=new ClientReceiver(socket);
cr.start();
//向客户端发送消息
boolean flag=true;
Scanner input=new Scanner(System.in);
while(true) {
while(flag) {
System.out.println("协议:群聊前缀“all:”私聊“人名:”,登录“login:”");
flag=false;
}
System.out.println("请输入消息:");
String message=input.next();
byte[] buffer=message.getBytes();
//使用输出流发送消息给服务器
OutputStream os=socket.getOutputStream();
os.write(buffer);
}
}
}
class ClientReceiver extends Thread{
private Socket socket;
public ClientReceiver(Socket socket) {
super();
this.socket = socket;
}
@Override
public void run() {
try {
while(true) {
InputStream in=socket.getInputStream();
byte[] buffer=new byte[1000];
int r=in.read(buffer);
System.out.println(new String(buffer,0,r));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package qqserver;
import java.net.Socket;
public class Customer {
private String username;
private Socket socket;
public Customer(String username, Socket socket) {
super();
this.username = username;
this.socket = socket;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
}