hello/hi的简单的网络聊天程序
hello/hi的简单的网络聊天程序
0 Linux Socket API
Berkeley套接字接口,一个应用程序接口(API),使用一个Internet套接字的概念,使主机间或者一台计算机上的进程间可以通讯。 它可以在很多不同的输入/输出设备和驱动之上运行,尽管这有赖于操作系统的具体实现。 接口实现用于TCP/IP协议,因此它是维持Internet的基本技术之一。 它是由加利福尼亚的伯克利大学开发,最初用于Unix系统。 如今,所有的现代操作系统都有一些源于Berkeley套接字接口的实现,它已成为连接Internet的标准接口。
我们所用的Linux Socket API实际上就是 Berkeley Socket,下面给出一些建立TCP连接常用的Socket接口函数及其功能概要:
socket()
创建一个新的确定类型的套接字,类型用一个整型数值标识(文件描述符),并为它分配系统资源。bind()
一般用于服务器端,将一个套接字与一个套接字地址结构相关联,比如,一个指定的本地端口和IP地址。listen()
用于服务器端,使一个绑定的TCP套接字的tcp状态由CLOSE转至LISTEN;操作系统内核为此监听socket所对应的tcp服务器创建一个pending socket队列和一个established socket队列;参数backlog指定pending socket队列的长度,0表示长度可以无限大。pending socket,就是某客户端三次握手的syn包到达,内核为这个syn包对应的tcp请求生成一个socket(状态为SYN_RECV),但三次握手还没有完成时的socket。connect()
用于客户端,为一个套接字分配一个自由的本地端口号。 如果是TCP套接字的话,它会试图获得一个新的TCP连接。accept()
用于服务器端。 它接受一个从远端客户端发出的创建一个新的TCP连接的接入请求,创建一个新的套接字,与该连接相应的套接字地址相关联。send()
和recv()
,或者write()
和read()
,或者recvfrom()
和sendto()
, 用于往/从远程套接字发送和接受数据。close()
用于系统释放分配给一个套接字的资源。 如果是TCP,连接会被中断。
1 hello/hi程序
1.0 功能概述
下面提供一个使用Java语言的简单的hello/hi程序代码,程序分为两部分,分别是服务端和客户端。该程序利用Java Socket API建立一个TCP连接,先从客户端向服务端发送一个“Hi,I am Client”字符串,然后服务端再返回一个“Hi,I am Server”字符串。
1.1 服务端
服务端的功能是使用Java的ServerSocket类的对象监听8090端口,然后使用ServerSocket类的accept()与8090端口建立连接,当收到一个来自客户端的"Hi,I am Client"时,回复一个"Hi,I am Server",代码如下:
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket socket = new ServerSocket(8090);// 监听8090端口
System.out.println("TCP server ready.");
Socket sock = socket.accept();// 建立连接
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8))) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) {
String cmd = reader.readLine();// 读取发送到服务端的数据
if ("Hi,I am Client".equals(cmd)) {
System.out.println("Client:"+cmd+"\n");
writer.write("Hi,I am Server"+ "\n");
writer.flush();
} else {
writer.write("require data\n");
writer.flush();
}
}
}
sock.close();// 关闭连接
socket.close();// 关闭监听端口
}
}
1.2 客户端
客户端的功能是获取本机地址,然后与本机的8090端口建立连接。向服务端发送"Hi,I am Client"后等待服务的回应的消息。
public class Client {
public static void main(String[] args) throws IOException {
InetAddress addr = InetAddress.getLoopbackAddress();// 获取本机地址,即“127.0.0.1”
try (Socket socket = new Socket(addr, 8090)) {// 与本机8090端口建立连接
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write("Hi,I am Client\n");
writer.flush();
String resp = reader.readLine();// 读取本机8090端口返回的数据
System.out.println("Server: " + resp);
}
}
}
}
}
2 分析Java Socket API和Linux Socket API的关系
上面两段程序调用ServerSocket类的构造方法和close()
,还有Socket类的构造方法、accept()
、getInputStream()
、``getOutputStream()和
close()`方法。下面通过对ServerSocket类以及Socket类的源码分析找到Java Socket API和Linux Socket API的对应关系。
2.0 SocketImpl类与Linux Socket API的关系
抽象类SocketImpl是实际上实现套接字的所有类的通用超类,它将实际的Socket操作抽象出来,在逻辑上与Linux Socket API的核心操作相对应,如果找到SocketImpl类的操作,就相当于找到了对应的Linux Socket API操作。
2.1 ServerSocket类
2.1.0 SeverSocket()
在服务端程序中,构造了一个ServerSocket类对象监听8090端口:
ServerSocket socket = new ServerSocket(8090);
找到相应的源码:
public ServerSocket(int port) throws IOException {
this(port, 50, (InetAddress)null);
}
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
this.created = false;
this.bound = false;
this.closed = false;
this.closeLock = new Object();
this.oldImpl = false;
this.setImpl();
if (port >= 0 && port <= 65535) {
if (backlog < 1) {
backlog = 50;
}
try {
this.bind(new InetSocketAddress(bindAddr, port), backlog);
} catch (SecurityException var5) {
this.close();
throw var5;
} catch (IOException var6) {
this.close();
throw var6;
}
} else {
throw new IllegalArgumentException("Port value out of range: " + port);
}
可以看到,this.bind(new InetSocketAddress(bindAddr, port), backlog);
语句中调用了bind()
方法,查看其具体实现:
this.getImpl().bind(epoint.getAddress(), epoint.getPort());
this.getImpl().listen(backlog);
发现SeverSocket.bind()
方法与Linux Socket API中的bind()
和listen()
函数相对应。
所以ServerSocket()
方法对应的Linux Socket API是socket()
,bind()
,listen()
。
2.1.1 accept()
服务端中还使用ServerSocket.accept()
方法初始化了一个Socket对象,下面找到其源码:
public Socket accept() throws IOException {
if (this.isClosed()) {
throw new SocketException("Socket is closed");
} else if (!this.isBound()) {
throw new SocketException("Socket is not bound yet");
} else {
Socket s = new Socket((SocketImpl)null);
this.implAccept(s);
return s;
}
}
发现其对应的Linux Socket API是accept()
。
2.1.2 close()
public void close() throws IOException {
synchronized(this.closeLock) {
if (!this.isClosed()) {
if (this.created) {
this.impl.close();
}
this.closed = true;
}
}
}
显然,ServerSocket.close()
对应的Linux Socket API是close()
。
2.2 Socket类
在客户端和服务端分别构造了一个Socket类对象用于建立Tcp连接和通信。下面分析所用函数的源码。
2.2.0 Socket()
客户端和服务端使用的构造函数分别是Socket(SocketImpl impl)
和Socket(InetAddress address, int port)
,前者其实是复制了一个Socket类的对象,只需看后者的源码即可:
public Socket(InetAddress address, int port) throws IOException {
this(address != null ? new InetSocketAddress(address, port) : null, (SocketAddress)null, true);
}
接着找真正的调用的构造函数:
private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException {
this.created = false;
this.bound = false;
this.connected = false;
this.closed = false;
this.closeLock = new Object();
this.shutIn = false;
this.shutOut = false;
this.oldImpl = false;
this.setImpl();
if (address == null) {
throw new NullPointerException();
} else {
try {
this.createImpl(stream);
if (localAddr != null) {
this.bind(localAddr);
}
this.connect(address);
} catch (IllegalArgumentException | SecurityException | IOException var7) {
try {
this.close();
} catch (IOException var6) {
var7.addSuppressed(var6);
}
throw var7;
}
}
}
发现该方法调用了Socket类的bind()
和connect()
方法。
根据前面已经知道,ServerSocket类的ServerSocket.bind()
方法与Linux Socket API中的bind()
和listen()
函数相对应,那么在Socket类中是否是这么对应的呢?查看一下Socket.bind()
方法的核心语句:this.getImpl().bind(addr, port);
发现在Socket.bind()
方法对应只是对应Linux Socket API中的bind()
。
而Socket.connect()
方法的核心语句如下:
if (!this.oldImpl) {
this.impl.connect(epoint, timeout);
} else {
if (timeout != 0) {
throw new UnsupportedOperationException("SocketImpl.connect(addr, timeout)");
}
if (epoint.isUnresolved()) {
this.impl.connect(addr.getHostName(), port);
} else {
this.impl.connect(addr, port);
}
}
发现对应的Linux Socket API中的函数是connect()
。
由上面的分析得出,Socket()
对于的Linux Socket API是socket()
,bind()
,connect()
。
2.2.1 getInputStream() & getOutputStream()
像之前一样,找到对应的源码:
public InputStream getInputStream() throws IOException {
if (this.isClosed()) {
throw new SocketException("Socket is closed");
} else if (!this.isConnected()) {
throw new SocketException("Socket is not connected");
} else if (this.isInputShutdown()) {
throw new SocketException("Socket input is shutdown");
} else {
InputStream is = null;
try {
is = (InputStream)AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
public InputStream run() throws IOException {
return Socket.this.impl.getInputStream();
}
});
return is;
} catch (PrivilegedActionException var3) {
throw (IOException)var3.getException();
}
}
}
public OutputStream getOutputStream() throws IOException {
if (this.isClosed()) {
throw new SocketException("Socket is closed");
} else if (!this.isConnected()) {
throw new SocketException("Socket is not connected");
} else if (this.isOutputShutdown()) {
throw new SocketException("Socket output is shutdown");
} else {
OutputStream os = null;
try {
os = (OutputStream)AccessController.doPrivileged(new PrivilegedExceptionAction<OutputStream>() {
public OutputStream run() throws IOException {
return Socket.this.impl.getOutputStream();
}
});
return os;
} catch (PrivilegedActionException var3) {
throw (IOException)var3.getException();
}
}
}
可以看到,Socket.getInputStream()
和 Socket.getOutputStream()
对应的Linux Java API是recv()
和send()
。
2.2.2 close()
public synchronized void close() throws IOException {
synchronized(this.closeLock) {
if (!this.isClosed()) {
if (this.created) {
this.impl.close();
}
this.closed = true;
}
}
}
同样,Socket.close()
对应的Linux Java API是close()
。
3 总结
由上面的探究可知,Java Socket API和Linux Socket API的关系如下表:
Java Socket API | Linux Socket API |
---|---|
ServerSocket() |
socket() ,bind() ,listen() |
ServerSocket.accept() |
accept() |
ServerSocket.bind() |
bind() ,listen() |
ServerSocket.close() |
close() |
Socket() |
socket() ,bind() ,connect() |
Socket.bind() |
bind() ,connect() |
Socket.getInputStream() |
recv() |
Socket.getOutputStream() |
send() |
Socket.close() |
close() |
可以看到,Java Socket API对Linux Socket API进行了进一步封装,使之更容易根据不同情况进行TCP连接。
参考资料
https://www.cnblogs.com/abcboy/p/9769230.html
https://github.com/mengning/net
https://zh.wikipedia.org/wiki/Berkeley%E5%A5%97%E6%8E%A5%E5%AD%97