利用java的Socket实现一个简单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)
{
unsignedlonga[AUDITSC_ARGS];
unsignedlonga0, a1;interr;
unsignedintlen;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)returnerr;
a0= a[0];
a1= a[1];switch(call) {caseSYS_SOCKET:
err= __sys_socket(a0, a1, a[2]);break;caseSYS_BIND:
err= __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);break;caseSYS_CONNECT:
err= __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);break;caseSYS_LISTEN:
err=__sys_listen(a0, a1);break;caseSYS_ACCEPT:
err= __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);break;
...
...default:
err= -EINVAL;break;
}returnerr;
}
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
packagechat1;
importjava.io.DataInputStream;importjava.io.DataOutputStream;importjava.io.IOException;importjava.net.ServerSocket;importjava.net.Socket;importjava.net.UnknownHostException;
/** 简易在线聊天程序服务端*/
public classserver {public static void main(String[]args) throwsUnknownHostException,IOException{
System.out.println("---server---");//指定端口 使用ServerSocket创建服务器
ServerSocket server = new ServerSocket(666);//阻塞式等待连接
Socket client =server.accept();
System.out.println("一个客户端连接建立");//接收消息
DataInputStream dis = newDataInputStream(client.getInputStream());
String msg=dis.readUTF();
System.out.println("client say:"+msg);//返回消息
DataOutputStream dos= newDataOutputStream(client.getOutputStream());
dos.writeUTF(msg);//释放资源
dos.flush();
dos.close();
dis.close();
client.close();
}
}
packagechat1;
importjava.io.BufferedReader;importjava.io.DataInputStream;importjava.io.DataOutputStream;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.Socket;importjava.net.UnknownHostException;
/** 简易在线聊天程序客户端*/
public classclient {public static void main(String[]args) throwsUnknownHostException, IOException{
System.out.println("---client---");//连接建立,使用Socket创建客户端,这里要注意端口号要跟本地其它已经写过的网络程序相区分开
Socket client =new Socket("localhost",666);//客户端发送消息
BufferedReader console=new BufferedReader(newInputStreamReader(System.in));
String msg=console.readLine();
DataOutputStream dos= newDataOutputStream(client.getOutputStream());
dos.writeUTF(msg);
dos.flush();//接收消息
DataInputStream dis = newDataInputStream(client.getInputStream());
msg=dis.readUTF();
System.out.println(msg);//释放资源
dos.close();
dis.close();
client.close();
}
}
这里只有一个客户端和一个服务端通信,也比较简单,就是服务端把客户端发来的消息再返回给客户端。我们来看一下运行结果吧。
版本二:一个服务器和多个客户端通信
Multiserver.java
packagechat2;
importjava.io.BufferedReader;importjava.io.Closeable;importjava.io.DataInputStream;importjava.io.DataOutputStream;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.ServerSocket;importjava.net.Socket;
/** 简易在线聊天程序服务器端,实现多个客户消息发送*/
public classMultiserver {public static void main(String[] args)throwsIOException{
System.out.println("----server---");//指定端口666,使用SeverSocekt创建服务器
ServerSocket server =new ServerSocket(666);//阻塞式监听,等待连接
while(true){
Socket client=server.accept();
System.out.println("一个客户端连接建立");new Thread(newChannel(client)).start();
}
}//为了多个响应多个客户,封装成多线程
static class Channel implementsRunnable{privateSocket client;//输入输出流封装
privateDataInputStream dis;privateDataOutputStream dos;private booleanisRuning;//构造器
public Channel(Socket client) throwsIOException{this.client=client;try{//输入流
dis = newDataInputStream(client.getInputStream());//输出流
dos = newDataOutputStream(client.getOutputStream());
isRuning= true;
}catch(IOException e) {//TODO Auto-generated catch block
e.printStackTrace();
release();
}
}//接收数据
privateString receive() {
String msg= "";try{
msg=dis.readUTF();
}catch(IOException e) {
e.printStackTrace();
}returnmsg;
}//发送数据
private voidsend(String msg) {
System.out.println(msg);try{
dos.writeUTF(msg);
dos.flush();
}catch(IOException e) {
e.printStackTrace();
}
}//释放资源
private void release() throwsIOException{this.isRuning=false;
Util.close(dis,dos);
client.close();
}
@Overridepublic voidrun() {while(isRuning){
String msg=receive();if(!msg.equals("")){
send(msg);
}
}
}
}
}
Multiclient.java
packagechat2;
importjava.io.BufferedReader;importjava.io.DataInputStream;importjava.io.DataOutputStream;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.Socket;importjava.net.UnknownHostException;
/** 简易在线聊天程序客户端,实现多个客户消息发送*/
public classMulticlient {public static void main(String[]args) throwsUnknownHostException, IOException{
System.out.println("---client---");//连接建立,使用Socket创建客户端,这里要注意端口号要跟本地其它已经写过的网络程序相区分开
Socket client =new Socket("localhost",666);//发送消息
new Thread(newsend(client)).start();//接收消息
new Thread(newreceive(client)).start();
}
}
receive.java
packagechat2;
importjava.io.DataInputStream;importjava.io.IOException;importjava.net.Socket;
/** 使用多线程封装接收端*/
public class receive implementsRunnable{privateDataInputStream dis;privateSocket client;private boolean isRuning = true;publicreceive(Socket client){this.client =client;try{
dis= newDataInputStream(client.getInputStream());
}catch(IOException e) {
e.printStackTrace();this.release();
}
}
@Overridepublic voidrun() {while(isRuning){
String msg=receive();if(!msg.equals("")){//System.out.println(msg);
}
}
}//接收数据
privateString receive() {
String msg= "";try{
msg=dis.readUTF();
}catch(IOException e) {
e.printStackTrace();
}returnmsg;
}//释放资源
private voidrelease(){this.isRuning = false;
Util.close(dis);try{
client.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
send.java
packagechat2;
importjava.io.BufferedReader;importjava.io.DataOutputStream;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.Socket;
/** 使用多线程封装发送端*/
public class send implementsRunnable{privateBufferedReader console;privateDataOutputStream dos;privateSocket client;private boolean isRuning = true;//构造器
publicsend(Socket client){this.client =client;
console= new BufferedReader(newInputStreamReader(System.in));try{
dos= newDataOutputStream(client.getOutputStream());
}catch(IOException e) {
e.printStackTrace();this.release();
}
}
@Overridepublic voidrun() {while(isRuning){
String msg=getStrFromConsole();if(!msg.equals("")){
send(msg);
}
}
}//从控制台获取消息
privateString getStrFromConsole(){try{returnconsole.readLine();
}catch(IOException e) {
e.printStackTrace();
}return "";
}//发送消息
private voidsend(String msg){
System.out.println(msg);try{
dos.writeUTF(msg);
dos.flush();
}catch(IOException e) {
e.printStackTrace();
}
}//释放资源
private voidrelease(){this.isRuning = false;
Util.close(dos);try{
client.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
Util.java
packagechat2;
importjava.io.Closeable;importjava.io.DataInputStream;importjava.io.DataOutputStream;importjava.net.Socket;
/** 工具类*/
public classUtil {/** 释放资源*/
public static voidclose(Closeable...targets){for(Closeable target:targets){try{if(null!=target)
target.close();
}catch(Exception e){
}
}
}
}
这里服务器是阻塞式监听多个客户端的,为了响应多个客户,封装成了多线程。每个客户端来请求服务的时候都是一个channel,而服务器在和一个客户端建立连接后又可以和其它的客户端建立连接。把具体的响应过程全部写到了run()方法中。封装了一个单独的工具类Util用于释放资源。把接收消息和发送消息单独封装成一个类,方便使用。且有利于代码的维护和重写。我们来看看运行结果吧:
本来还有一个版本三,实现了群聊,但是有一点小bug,这里就不展示了。作为一个java小白,好不容易写出来的。