简单网络聊天程序java_基于Java实现hello/hi简单网络聊天程序
Socket簡要闡述
Socket的概念
Socket的英文原義是“孔”或“插座”。
在網絡編程中,網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個Socket。
Socket套接字是通信的基石,是支持TCP/IP協議的網絡通信的基本操作單元。
它是網絡通信過程中端點的抽象表示,包含進行網絡通信必須的五種信息:連接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。
Socket本質是編程接口(API),對TCP/IP的封裝,TCP/IP也要提供可供程序員做網絡開發所用的接口,這就是Socket編程接口。
HTTP是轎車,提供了封裝或者顯示數據的具體形式;Socket是發動機,提供了網絡通信的能力。
Socket原理
Socket實質上提供了進程通信的端點。進程通信之前,雙方首先必須各自創建一個端點,否則是沒有辦法建立聯系并相互通信的。正如打電話之前,雙方必須各自擁有一臺電話機一樣。
套接字之間的連接過程可以分為三個步驟:服務器監聽,客戶端請求,連接確認。
服務器監聽:建立服務器端套接字,并處于等待連接的狀態,不定位具體的客戶端套接字,而是實時監控網絡狀態。
客戶端請求:是指由客戶端的套接字提出連接請求,要連接的目標是服務器端的套接字。
為此,客戶端的套接字必須首先描述它要連接的服務器的套接字,指出服務器端套接字的地址和端口號,然后就向服務器端套接字提出連接請求。
連接確認:是指當服務器端套接字監聽到或者說接收到客戶端套接字的連接請求,它就響應客戶端套接字的請求,建立一個新的線程,把服務器端套接字的描述發給客戶端,
一旦客戶端確認了此描述,連接就建立好了。而服務器端套接字繼續處于監聽狀態,繼續接收其他客戶端套接字的連接請求。
下圖為基于TCP協議Socket的通信模型。
hello/hi的簡單網絡聊天程序實現
服務器端
實現步驟
1.創建ServerSocket對象,綁定監聽端口。
2.通過accept()方法監聽客戶端請求。
3.連接建立后,在接收進程中通過輸入流讀取客戶端發送的請求信息。
4.在服務器發送進程中通過輸出流向客戶端發送響應信息。
5.關閉相應的資源和Socket。
package com.socket.MultiThread;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class Server {
public static ServerSocket serverSocket;
public static Socket socket;
public static Scanner scanner;
/**
* 構造方法
* 新建serverSocket和Socket
*/
public Server() {
try {
serverSocket = new ServerSocket(6666);
System.out.println("Server is working, waiting for client's link");
socket = serverSocket.accept();
System.out.println("Client has linked with Server");
} catch (IOException i) {
i.printStackTrace();
}
}
/**
* 服務器端發送消息線程
* 作用:從鍵盤讀入消息,發送給服務器端
*/
public class SendThread implements Runnable {
@Override
public void run() {
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);
scanner = new Scanner(System.in);
while (true) {
printWriter.println(scanner.nextLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 服務器端接收線程
* 作用:使用字符流讀取緩沖區中客戶端所發送的消息
*/
public class ReceiveThread implements Runnable {
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 輸出從客戶端端接收到的消息
while (true) {
System.out.println("Client> " + bufferedReader.readLine());
}
} catch (IOException i) {
i.printStackTrace();
}
}
}
public void start() {
Thread send = new Thread(new SendThread()); // 發送進程負責服務器端的消息發送
Thread receive = new Thread(new ReceiveThread()); // 接收進程負責接收客戶端的消息
send.start();
receive.start();
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客戶端
實現步驟
1.創建Socket對象,指明需要連接的服務器的地址和端口號。
2.連接建立后,通過輸出流向服務器發送請求信息。
3.通過輸入流獲取服務器響應的信息。
4.關閉相應資源。
package com.socket.MultiThread;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static Socket socket;
public static Scanner scanner;
/**
* 構造方法
* 新建一個socket,并指定了host和port屬性,其port與服務器端保持一致
*/
public Client() {
try {
socket = new Socket("127.0.0.1", 6666);
} catch (IOException i) {
i.printStackTrace();
}
}
/**
* 客戶端發送消息線程
* 作用:從鍵盤讀入消息,發送給服務器端
*/
public class SendThread implements Runnable {
@Override
public void run() {
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);
scanner = new Scanner(System.in);
while (true) {
printWriter.println(scanner.nextLine());
}
} catch (IOException i) {
i.printStackTrace();
}
}
}
/**
* 客戶端接收線程
* 作用:使用字符流讀取緩沖區中服務器端所發送的消息
*/
public class ReceiveThread implements Runnable {
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 輸出從服務器端接收到的消息
while (true) {
System.out.println("Server> " + bufferedReader.readLine());
}
} catch (IOException i) {
i.printStackTrace();
}
}
}
public void start() {
Thread send = new Thread(new SendThread()); // 發送進程負責客戶端的消息發送
Thread receive = new Thread(new ReceiveThread()); // 接收進程負責接收服務器端的消息
send.start();
receive.start();
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
程序執行結果
先運行服務器端,后運行客戶端,服務器端在監聽客戶端的連接請求后建立連接。
服務器端與客戶端的交互
跟蹤分析調用棧 & Linux API對比
創建ServerSocket
在前面的服務器端代碼中,我們創建一個ServerSocket是這樣做的:
serverSocket = new ServerSocket(6666);
這行代碼在平??磥砭褪莿摻艘粋€端口號為6666的ServerSocket。
但是實際上,我們只是調用了大牛們早已經寫好并封裝在JDK中的方法,這才能夠如此簡單地完成套接字的創建。
因此下面通過查看JDK源碼,追蹤其調用棧來看看ServerSocket的創建究竟是如何實現的。
調用棧圖示
通過上圖中對于jdk代碼中socket創建過程的展示,我們了解到:
在java中ServerSocket的創建主要是調用PlainSocketImpl.socketCreate這個native方法來實現的。
源碼分析
那么,我們來康一下這個方法:
/**
* The file descriptor object for this socket.
*/
protected FileDescriptor fd; // 文件描述符
@Override
void socketCreate(boolean stream) throws IOException {
if (fd == null) // 空則拋出異常
throw new SocketException("Socket closed");
int newfd = socket0(stream); // 調用jvm的socket0方法來創建新的fd
fdAccess.set(fd, newfd);
}
可以看到PlainSocketImpl.socketCreate方法中有一個重要的變量是fd,在代碼塊中我也將這個變量的聲明一并列出了。
記得在本科的Linux課上老師曾經也著重強調了文件描述符這個概念,那么此fd是彼fd嗎?
在了解了Linux內核中Socket的建立之后,就能夠得出答案:是的。
在jvm中,調用linux底層api: socket()函數時,執行的步驟為:
創建socket結構體
創建tcp_sock結構體,剛創建完的tcp_sock的狀態為:TCP_CLOSE
創建文件描述符與socket綁定
因此,在PlainSocketImpl.socketCreate方法中所實現的也正是這樣的邏輯。
Socket綁定
上述分析中,我們會發現:
在PlainSocketImpl.socketCreate中創建socket時,它并沒有綁定任何的ip地址與端口,只是實現了與文件描述符的綁定。
這就有點奇怪了,我們在上面的Java代碼中創建ServerSocket的時候明明指定了端口號的呀,怎么調用到底層方法它就把端口號丟了呢?
再次分析源碼,原來僅僅是new ServerSocket(6666);這一步操作就調用了三次Linux API,其對應關系如下圖。
調用棧圖示
同樣的,我們可以得出:java中ServerSocket的綁定是調用PlainSocketImpl.socketBind這個native方法來實現的。
源碼分析
查看以下JDK源碼中PlainSocketImpl.socketBind方法的內容。
@Override
void socketBind(InetAddress address, int port) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (address == null) // ip地址為空則拋出異常
throw new NullPointerException("inet address argument is null.");
if (preferIPv4Stack && !(address instanceof Inet4Address)) // 限定IP地址為IPv4版本
throw new SocketException("Protocol family not supported");
// 調用jvm的bind0方法實現綁定
bind0(nativefd, address, port, useExclusiveBind);
if (port == 0) { // 沒有給出端口號
localport = localPort0(nativefd);
} else {
localport = port;
}
this.address = address;
}
可以看到,在上面的方法中通過調用bind0這個方法來實現實現的端口號以及IP地址的綁定。
并且,源碼限制目前所支持的IP地址是IPv4版本的(雖然目前IPv4地址已經分配完畢),相信在后續的JDK更新中這里會修改過來。
Socket監聽
從之前的Java調用Linux API圖中可以看到,在完成Socket的創建和綁定之后,服務器端進入監聽的狀態,等待客戶端發出連接的請求。
調用棧圖示
從上圖可以得出:java中ServerSocket的綁定是調用PlainSocketImpl.socketListen這個native方法來實現的。
源碼分析
@Override
void socketListen(int backlog) throws IOException {
int nativefd = checkAndReturnNativeFD();
// 調用jvm的listen0方法實現監聽
listen0(nativefd, backlog);
}
在JDK中監聽的實現較為簡單,主要是通過調用JVM中listen0來實現的,這里不做過多的展開。
Socket Accept
服務器端一直被動等待著客戶端的連接,終于有一個客戶端使用與之匹配的IP地址和端口號,
并在經歷了TCP三次握手之后,客戶端建立新的連接Socket對象,服務器就與這個客戶端建立了TCP連接。
調用棧圖示
從上圖可以得出:java中ServerSocket的綁定是調用PlainSocketImpl.socketAccept這個native方法來實現的。
源碼分析
@Override
void socketAccept(SocketImpl s) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null");
int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) { // 設定有超時計時器
// 沒有超時則調用accept0方法建立連接
newfd = accept0(nativefd, isaa);
} else {
// 否則將該客戶端掛入阻塞隊列中
configureBlocking(nativefd, false);
try {
waitForNewConnection(nativefd, timeout);
newfd = accept0(nativefd, isaa);
if (newfd != -1) {
configureBlocking(newfd, true);
}
} finally {
configureBlocking(nativefd, true);
}
}
// 更新socketImpl的文件描述符值
fdAccess.set(s.fd, newfd);
// 更新socketImpl中的端口號、ip地址以及localport值
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
if (preferIPv4Stack && !(s.address instanceof Inet4Address))
throw new SocketException("Protocol family not supported");
}
Java Socekt API與Linux Socket API
在上面的調用棧分析中,無論是ServerSocket的創建、綁定、監聽,還是連接都伴隨著對glibc的調用。
那么glibc到底何許人也?這里引用百度詞條的內容:
glibc是GNU發布的libc庫,即c運行庫。glibc是linux系統中最底層的api,幾乎其它任何運行庫都會依賴于glibc。glibc除了封裝linux操作系統所提供的系統服務外,它本身也提供了許多其它一些必要功能服務的實現。由于 glibc 囊括了幾乎所有的 UNIX 通行的標準,可以想見其內容包羅萬象。而就像其他的 UNIX 系統一樣,其內含的檔案群分散于系統的樹狀目錄結構中,像一個支架一般撐起整個操作系統。在 GNU/Linux 系統中,其C函式庫發展史點出了GNU/Linux 演進的幾個重要里程碑,用 glibc 作為系統的C函式庫,是GNU/Linux演進的一個重要里程碑。
就綁定功能而言,在上述的調用棧追蹤中我們知道了所調用的是底層由glibc提供的Bind方法,
但實際上,最終調用內核的SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)。
因此,可以得出結論:
java的socket實現是通過調用操作系統的socket api實現的
參考鏈接
總結
以上是生活随笔為你收集整理的简单网络聊天程序java_基于Java实现hello/hi简单网络聊天程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超声声场模拟_超声全聚焦(TFM)简介
- 下一篇: 中期改款领克03亮相:嘴大了、灵魂尾灯没