利用java实现一个简单hello/hi聊天程序
首先,我们来用java实现一个简单的hello/hi聊天程序。在这个程序里,我学习到了怎么用socket套接套接字来进行编程。简单理解了一些关于socket套接字和底层调用的关系。关于java的封装思想,我学会了一些东西,java里真的是万物皆对象。还学到了一点多线程的知识。
TCP
在这里,不得不先介绍以下TCP。TCP是传输层面向连接的协议。提供了端到端的进程之间的通信方式。TCP在通信之前要先建立连接。这里我们称这个建立连接的过程为“三次握手”。如果想详细了解TCP建立连接和释放连接的过程,请参考我另一篇博客。
JavaSocket
Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。抽象类SocketImpl是实现套接字的所有类的通用超类。创建客户端和服务器套接字都可以使用它。
Java在调用Socket方法的时候并不能直接调用底层的函数。作为一个高度封装的语言。它只能先调用JVM,然后它在调用操作系统的内核函数来建立Socket连接。
Socket
这里我们打开了Linux下的Socket函数,这里我们查看到一些基本信息。
AF_INET这里用的是IPv4
SOCK_STREAM是流式套接字,基于面向字节流的TCP
SOCK_DGRAM是数据报套接字,基于面向数据报的UDP
Linux中万物皆文件,套接字只是其中文件之间的一种通信方式,虽然是不同主机上的文件。通信双方的主机各自打开一个套接字,套接字之间通过网络来连接,这样两个主机上的进程就可以交换文件信息了。
下面是socket调用的过程:
socket()调用sys_socketcall()系统调用。bind,connect等等函数都需要sys_socketcall()作为入口。
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
unsigned long a[AUDITSC_ARGS];
unsigned long a0, a1;
int err;
unsigned int len;
if (call < 1 || call > SYS_SENDMMSG)
return -EINVAL;
call = array_index_nospec(call, SYS_SENDMMSG + 1);
len = nargs[call];
if (len > sizeof(a))
return -EINVAL;
/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, len))
return -EFAULT;
err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
if (err)
return err;
a0 = a[0];
a1 = a[1];
switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
...
...
default:
err = -EINVAL;
break;
}
return err;
}
TCP编程
创建SeverSocket的几种构造方法:
SeverSocket(); //创建不绑定服务器的套接字
ServerSocket server = new ServerSocket(int port); //指定端口号绑定
ServerSocket(int port,int backlog); //指定的backlog创建服务器套接字并绑定到指定的本地端口号
ServerSocket(int port,int backlog,InetAddress bindAddr); //使用指定的端口,监听backlog和要绑定的本地IP地址创建服务器
Accept方法用于阻塞式等待客户端连接,等到一个连接后就返回一个客户端的Socket的对象实例。
Socket client = server.accept();
DataInputStream是用来获取输入流的相关信息的,返回一个DataInputStream对象的实例。
DataInputStream dis = new DataInputStream(client.getInputStream());
DataOutputStream用来操作输出流的相关信息,返回一个DataOutputStream对象实例。
DataOutputStream dos= new DataOutputStream(client.getOutputStream());
以下是一些方法:
方法摘要 | |
---|---|
void | bind(SocketAddress bindpoint) 将套接字绑定到本地地址。 |
void | close() 关闭此套接字。 |
void | connect(SocketAddress endpoint) 将此套接字连接到服务器。 |
void | connect(SocketAddress endpoint, int timeout) 将此套接字连接到服务器,并指定一个超时值。 |
SocketChannel | getChannel() 返回与此数据报套接字关联的唯一 SocketChannel 对象(如果有)。 |
InetAddress | getInetAddress() 返回套接字连接的地址。 |
InputStream | getInputStream() 返回此套接字的输入流。 |
boolean | getKeepAlive() 测试是否启用 SO_KEEPALIVE。 |
InetAddress | getLocalAddress() 获取套接字绑定的本地地址。 |
int | getLocalPort() 返回此套接字绑定到的本地端口。 |
SocketAddress | getLocalSocketAddress()返回此套接字绑定的端点的地址,如果尚未绑定则返回 null`。 |
boolean | getOOBInline() 测试是否启用 OOBINLINE。 |
OutputStream | getOutputStream() 返回此套接字的输出流。 |
int | getPort() 返回此套接字连接到的远程端口。 |
int | getReceiveBufferSize()获取此 Socket的 SO_RCVBUF 选项的值,该值是平台在 Socket` 上输入时使用的缓冲区大小。 |
SocketAddress | getRemoteSocketAddress()返回此套接字连接的端点的地址,如果未连接则返回 null`。 |
boolean | getReuseAddress() 测试是否启用 SO_REUSEADDR。 |
int | getSendBufferSize()获取此 Socket的 SO_SNDBUF 选项的值,该值是平台在 Socket` 上输出时使用的缓冲区大小。 |
int | getSoLinger() 返回 SO_LINGER 的设置。 |
int | getSoTimeout() 返回 SO_TIMEOUT 的设置。 |
boolean | getTcpNoDelay() 测试是否启用 TCP_NODELAY。 |
int | getTrafficClass() 为从此 Socket 上发送的包获取 IP 头中的流量类别或服务类型。 |
boolean | isBound() 返回套接字的绑定状态。 |
boolean | isClosed() 返回套接字的关闭状态。 |
boolean | isConnected() 返回套接字的连接状态。 |
boolean | isInputShutdown() 返回是否关闭套接字连接的半读状态 (read-half)。 |
boolean | isOutputShutdown() 返回是否关闭套接字连接的半写状态 (write-half)。 |
void | sendUrgentData(int data) 在套接字上发送一个紧急数据字节。 |
void | setKeepAlive(boolean on) 启用/禁用 SO_KEEPALIVE。 |
void | setOOBInline(boolean on) 启用/禁用 OOBINLINE(TCP 紧急数据的接收者) 默认情况下,此选项是禁用的,即在套接字上接收的 TCP 紧急数据被静默丢弃。 |
void | setPerformancePreferences(int connectionTime, int latency, int bandwidth)` 设置此套接字的性能偏好。 |
void | setReceiveBufferSize(int size)将此 Socket` 的 SO_RCVBUF 选项设置为指定的值。 |
void | setReuseAddress(boolean on) 启用/禁用 SO_REUSEADDR 套接字选项。 |
void | setSendBufferSize(int size)将此 Socket` 的 SO_SNDBUF 选项设置为指定的值。 |
static void | setSocketImplFactory(SocketImplFactory fac) 为应用程序设置客户端套接字实现工厂。 |
void | setSoLinger(boolean on, int linger) 启用/禁用具有指定逗留时间(以秒为单位)的 SO_LINGER。 |
void | setSoTimeout(int timeout) 启用/禁用带有指定超时值的 SO_TIMEOUT,以毫秒为单位。 |
void | setTcpNoDelay(boolean on) 启用/禁用 TCP_NODELAY(启用/禁用 Nagle 算法)。 |
void | setTrafficClass(int tc) 为从此 Socket 上发送的包在 IP 头中设置流量类别 (traffic class) 或服务类型八位组 (type-of-service octet)。 |
void | shutdownInput() 此套接字的输入流置于“流的末尾”。 |
void | shutdownOutput() 禁用此套接字的输出流。 |
String | toString()将此套接字转换为 String。 |
版本一:一个服务器和一个客户端通信
server.java
package chat1;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
/*
* 简易在线聊天程序服务端
*/
public class server {
public static void main(String[]args) throws UnknownHostException,IOException{
System.out.println("---server---");
//指定端口 使用ServerSocket创建服务器
ServerSocket server = new ServerSocket(666);
//阻塞式等待连接
Socket client = server.accept();
System.out.println("一个客户端连接建立");
//接收消息
DataInputStream dis = new DataInputStream(client.getInputStream());
String msg = dis.readUTF();
System.out.println("client say:"+msg);
//返回消息
DataOutputStream dos= new DataOutputStream(client.getOutputStream());
dos.writeUTF(msg);
//释放资源
dos.flush();
dos.close();
dis.close();
client.close();
}
}
package chat1;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
/*
* 简易在线聊天程序客户端
*/
public class client {
public static void main(String[]args) throws UnknownHostException, IOException{
System.out.println("---client---");
//连接建立,使用Socket创建客户端,这里要注意端口号要跟本地其它已经写过的网络程序相区分开
Socket client =new Socket("localhost",666);
//客户端发送消息
BufferedReader console=new BufferedReader(new InputStreamReader(System.in));
String msg = console.readLine();
DataOutputStream dos= new DataOutputStream(client.getOutputStream());
dos.writeUTF(msg);
dos.flush();
//接收消息
DataInputStream dis = new DataInputStream(client.getInputStream());
msg = dis.readUTF();
System.out.println(msg);
//释放资源
dos.close();
dis.close();
client.close();
}
}
这里只有一个客户端和一个服务端通信,也比较简单,就是服务端把客户端发来的消息再返回给客户端。我们来看一下运行结果吧。
版本二:一个服务器和多个客户端通信
Multiserver.java
package chat2;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/*
* 简易在线聊天程序服务器端,实现多个客户消息发送
*/
public class Multiserver {
public static void main(String[] args)throws IOException{
System.out.println("----server---");
//指定端口666,使用SeverSocekt创建服务器
ServerSocket server =new ServerSocket(666);
//阻塞式监听,等待连接
while(true){
Socket client=server.accept();
System.out.println("一个客户端连接建立");
new Thread(new Channel(client)).start();
}
}
//为了多个响应多个客户,封装成多线程
static class Channel implements Runnable{
private Socket client;
//输入输出流封装
private DataInputStream dis;
private DataOutputStream dos;
private boolean isRuning;
//构造器
public Channel(Socket client) throws IOException{
this.client=client;
try {
//输入流
dis = new DataInputStream(client.getInputStream());
//输出流
dos = new DataOutputStream(client.getOutputStream());
isRuning = true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
release();
}
}
//接收数据
private String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
}
return msg;
}
//发送数据
private void send(String msg) {
System.out.println(msg);
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
//释放资源
private void release() throws IOException{
this.isRuning=false;
Util.close(dis,dos);
client.close();
}
@Override
public void run() {
while(isRuning){
String msg = receive();
if(!msg.equals("")){
send(msg);
}
}
}
}
}
Multiclient.java
package chat2;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
/*
* 简易在线聊天程序客户端,实现多个客户消息发送
*/
public class Multiclient {
public static void main(String[]args) throws UnknownHostException, IOException{
System.out.println("---client---");
//连接建立,使用Socket创建客户端,这里要注意端口号要跟本地其它已经写过的网络程序相区分开
Socket client =new Socket("localhost",666);
//发送消息
new Thread(new send(client)).start();
//接收消息
new Thread(new receive(client)).start();
}
}
receive.java
package chat2;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/*
* 使用多线程封装接收端
*/
public class receive implements Runnable{
private DataInputStream dis;
private Socket client;
private boolean isRuning = true;
public receive(Socket client){
this.client = client;
try {
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
e.printStackTrace();
this.release();
}
}
@Override
public void run() {
while(isRuning){
String msg = receive();
if(!msg.equals("")){
//System.out.println(msg);
}
}
}
//接收数据
private String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
}
return msg;
}
//释放资源
private void release(){
this.isRuning = false;
Util.close(dis);
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
send.java
package chat2;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/*
* 使用多线程封装发送端
*/
public class send implements Runnable{
private BufferedReader console;
private DataOutputStream dos;
private Socket client;
private boolean isRuning = true;
//构造器
public send(Socket client){
this.client = client;
console = new BufferedReader(new InputStreamReader(System.in));
try {
dos= new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
this.release();
}
}
@Override
public void run() {
while(isRuning){
String msg = getStrFromConsole();
if(!msg.equals("")){
send(msg);
}
}
}
//从控制台获取消息
private String getStrFromConsole(){
try {
return console.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
//发送消息
private void send(String msg){
System.out.println(msg);
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
//释放资源
private void release(){
this.isRuning = false;
Util.close(dos);
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Util.java
package chat2;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
/*
* 工具类
*/
public class Util {
/*
* 释放资源
*/
public static void close(Closeable...targets){
for(Closeable target:targets){
try{
if(null!=target)
target.close();
}catch(Exception e){
}
}
}
}
这里服务器是阻塞式监听多个客户端的,为了响应多个客户,封装成了多线程。每个客户端来请求服务的时候都是一个channel,而服务器在和一个客户端建立连接后又可以和其它的客户端建立连接。把具体的响应过程全部写到了run()方法中。封装了一个单独的工具类Util用于释放资源。把接收消息和发送消息单独封装成一个类,方便使用。且有利于代码的维护和重写。我们来看看运行结果吧:
本来还有一个版本三,实现了群聊,但是有一点小bug,这里就不展示了。作为一个java小白,好不容易写出来的。