先从一小段简单代码开始:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class MyServer {
static int port = 8888;
InputStream is;
OutputStream os;
public static void main(String[] args) throws IOException {
new MyServer().createServer(port);
}
public void createServer(int port) throws IOException {
//在本地建立一个服务器的套接字(套接字内容为本设备IP地址加port赋值的端口号)
ServerSocket server = new ServerSocket(port);
System.out.println("创建服务器成功,等待客户端链接。。。");
//监听链接此服务器的客户端,该方法为阻塞方法
Socket socket = server.accept();
//通过socket获取输入输出
is = socket.getInputStream();
os = socket.getOutputStream();
//为了达成自由的输入输出,这里使用了多线程
InputThread inPut = new InputThread();
OutputThread outPut = new OutputThread();
outPut.start();
inPut.start();
}
//输入流线程
private class InputThread extends Thread {
@Override
public void run() {
char c;
String[] s = new String[10];
String[] p = new String[10];
int i = 0;
while (true){
//读取客户发来的信息
try {
c = (char)is.read();
} catch (IOException e) {
throw new RuntimeException(e);
}
if(c == 13 ) {
s[i] = s[i] + "\r\n" ;
System.out.print(s[i]);
i++;
if(i % 10 == 0){
//数据记录的扩容
for(int j = 0 ; j < i ;j++){
p[j] = s[j];
}
s = new String[i + 10];
for(int j = 0 ; j < i ;j++){
s[j] = p[j];
}
p = new String[i + 10];}}
s[i] = s[i] + c;}}}
//输出流线程
private class OutputThread extends Thread{
@Override
public void run() {
Scanner sc = new Scanner(System.in);
String s ;
while (true){
s = sc.nextLine();
try {
s = s + "\r\n";
os.write(s.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}}}}
在运行该程序后,用WIN+R快捷键打开运行,输入cmd,打开命令指示框。
在命令指示框中输入:telnet localhost 8888(这段数字取决于代码中port的值),即可在命令指示框与代码运行窗口直接进行通信测试。
在这段程序中,数据通信的过程为基本的服务器-客户端双向通信:
程序中出现的数个概念解释:
服务器(Server):
一般情况下提供服务的一端
客户端(Clinic):
一般情况下接受服务的一端
TCP/IP协议:
全称 Transmission Control Protocol/Internet Protocol 传输控制协议/网际协议,是网络通信最基本的协议。
其中IP协议是指数据传输时,信息的发送端将数据传输到接收端。可以将网络通信想象成现实中的物流系统,IP协议就是:当某人(信息发送端)将包裹投入快递驿站后,整个物流系统会根据信封上的地址(IP地址)找到正确的收件人(接收端)。
TCP系统则可以视为物流系统的打包和签收服务。当包裹太大太重不好搬运时时,工作人员会对你的包裹进行拆解(TCP提供可靠字节流服务,即将大块数据拆解成以segment为单位的数段方面运输管理),并且安排送上门的签收服务。快递小哥在根据IP地址找到接收者地址后,会敲门询问:您是XXX吗?客户:是的。小哥:好的,请开一下门(建立联系)签收快递(数据传输)。(为保障数据可靠运输而设的步骤,程序上俗称“三次握手”)
端口(Port):借用上述使用过的快递例子,端口可被视为门牌号。当由客户端向服务器发出申请时,不同的数据应该被发送至服务器不同程序中进行处理。数据进入这些程序的虚拟入口就可以被称为端口。为方便区分查找为诸多端口编号,其序号就是端口号。
套接字(Socket,直译为 插座):输入输出流会通过socket进出服务器和客户端。
在组成上,socket = IP :Port
(例如IP 地址=192.201.1.234,端口 = 31,则socket = 192.201.1.234.31)
故可以通过对socket的操作获取IO流
接下来就该实现多人围绕本地服务器的聊天室功能:
将原来的一服务器对一客户端的流程封装成一个独立线程,并通过共有的socket表和user表实现私聊和群聊,本代码属于想到哪里写到哪里,所以显得比较驳杂
import javax.sound.sampled.LineListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
public class MyServer {
static int port = 8888;
private InputStream is;
private OutputStream os;
private int num = 0;
private List<String> privateList = new LinkedList<>();
private List<Socket> socketList = new LinkedList<>();
private List<User> userList = new LinkedList<>();
private boolean privateChat = false;
public static void main(String[] args) throws IOException {
new MyServer().createServer(port);
}
public void createServer(int port) throws IOException {
ServerSocket server = new ServerSocket(port);
System.out.println("The server is successfully founded,waiting for users'connecting request。。。");
//监听链接此服务器的客户端,该方法为阻塞方法
int num = 0;
while(true){
Socket socket = server.accept();
socketList.add(socket);
Runnable run = new usersThread(socket,num);
Thread thread = new Thread(run);
thread.start();
num++;
}
}
//将字节流转换成字符串
public String readMsg(InputStream is )throws IOException{
StringBuilder stringBuilder = new StringBuilder();
int i = 0;int j = 0;
String s = "";
boolean check = false;
while((i = is.read()) != 13){
if(i == '@'){
check = !check;
if(!check) {
privateList.add(s);
s = "";
privateChat = true;
}
}else if(check){
s = s + (char)i;
}
stringBuilder.append((char)i);
}
return stringBuilder.toString().trim();
}
//创建用户信息
private class User{
public String nickName;
public Socket socket;
private int num;
public User(String nickName,Socket socket,int num){
this.nickName = nickName;
this.socket = socket;
this.num = num;
}
}
private class usersThread implements Runnable{
private InputStream is ;
private User user;
private OutputStream os ;
//单个用户线程
public usersThread(Socket socket,int num) throws IOException {
//短期账户创建步骤(登记昵称)
is = socket.getInputStream();
os =socket.getOutputStream();
String msg = "Please enter your temporary nickname:\r\n";
os.write(msg.getBytes());
user = new User(readMsg(is) ,socket,num);
userList.add(user);
msg ="Welcome" + " " + user.nickName + "\r\n";
os.write(msg.getBytes());
}
@Override
public void run() {
System.out .println("user:" + user.nickName + " enter the room");
while(true){
try{
String s = readMsg(is);
System.out.println("user " + user.nickName + " says:" + s + "\r\n");
for(int i = 0;i <socketList.size();i++){
Socket socket = socketList.get(i);
//私聊代码
if(privateChat){
checkIfExist(user.socket,s);
privateChat = false;
privateList = new LinkedList<String>();
break;
}
//群聊代码
if(socket != user.socket){
os = socket.getOutputStream();
os.write(("user " + user.nickName + " says: " + s + "\r\n").getBytes());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
//私聊对象是否存在?
private void checkIfExist(Socket socket,String s) throws IOException {
for(int i = 0;i < privateList.size();i++){
for(int j = 0 ; j < userList.size();j++){
//遍历寻找私聊对象,获取其socket值
if(privateList.get(i).equals(userList.get(j).nickName)){
os = userList.get(j).socket.getOutputStream();
os.write(("user " + userList.get(j).nickName + " says to you privately: " + s + "\r\n").getBytes());
break;
}
System.out.print(privateList.get(i)+":");
System.out.println(userList.get(j).nickName);
}
os = socket.getOutputStream();
os.write((privateList.get(i)+" doesn't exist.\r\n").getBytes());
}
}
}
在测试该段代码时,如果某客户端断开连接,会标识报错:Space Heap。其原因是我们使用的客户端与所写的程序底层存在不兼容。经测试,如果是自己写地客户端就不会报该错误。