前言
我們做前端開發主要用http/https請求,這種請求從數據更新角度是單向的,即用戶發起請求才能獲取到最新數據。但有時候,一些狀態和數據的變更要及時推送到前端。例如O2O行業,消費者下定單 -> O2O公司接受到訂單 -> 送外賣小哥即時收到訂單-> 消費者實時收到外賣小哥和自己的距離。
其中,后兩步要即時收到信息,就得利用 Socket編程保持長連接。再比如,消息推送,語音聊天等。
注意:
HTTP也可以建立長連接的,使用Connection:keep-alive,HTTP 1.1默認進行持久連接。HTTP1.1和HTTP1.0相比較而言,最大的區別就是增加了持久連接支持(貌似最新的 http1.0 可以顯示的指定 keep-alive),但還是無狀態的,或者說是不可以信任的。
1、網絡中進程之間如何通信?
本地的進程間通信(IPC)有很多種方式,但可以總結為下面4類:
- 消息傳遞(管道、FIFO、消息隊列)
- 同步(互斥量、條件變量、讀寫鎖、文件和寫記錄鎖、信號量)
- 共享內存(匿名的和具名的)
- 遠程過程調用(Solaris門和Sun RPC)
網絡中進程之間如何通信?
首要解決的問題是如何唯一標識一個進程,否則通信無從談起!在本地可以通過進程PID來唯一標識一個進程,但是在網絡中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機,而傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。
使用TCP/IP協議的應用程序通常采用應用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網絡進程之間的通信。
就目前而言,幾乎所有的應用程序都是采用socket,而現在又是網絡時代,網絡中進程通信是無處不在,這就是我為什么說“一切皆socket”。
2. Socket是什么
2.1 socket套接字:
socket起源于Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現, socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉).
說白了Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓編程變的更簡單。是一個軟件抽象層。在網絡編程中,我們大量用的都是通過socket實現的。
2.2、套接字描述符
其實就是一個整數,我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr
套接字API最初是作為UNIX操作系統的一部分而開發的,所以套接字API與系統的其他I/O設備集成在一起。特別是,當應用程序要為因特網通信而創建一個套接字(socket)時,操作系統就返回一個小整數作為描述符(descriptor)來標識這個套接字。然后,應用程序以該描述符作為傳遞參數,通過調用函數來完成某種操作(例如通過網絡傳送數據或接收輸入的數據)。
在許多操作系統中,套接字描述符和其他I/O描述符是集成在一起的,所以應用程序可以對文件進行套接字I/O或I/O讀/寫操作。
當應用程序要創建一個套接字時,操作系統就返回一個小整數作為描述符,應用程序則使用這個描述符來引用該套接字需要I/O請求的應用程序請求操作系統打開一個文件。操作系統就創建一個文件描述符提供給應用程序訪問文件。從應用程序的角度看,文件描述符是一個整數,應用程序可以用它來讀寫文件。下圖顯示,操作系統如何把文件描述符實現為一個指針數組,這些指針指向內部數據結構。
對于每個程序系統都有一張單獨的表。精確地講,系統為每個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統把一個指向此文件內部數據結構的指針寫入文件描述符表,并把該表的索引值返回給調用者 。應用程序只需記住這個描述符,并在以后操作該文件時使用它。操作系統把該描述符作為索引訪問進程描述符表,通過指針找到保存該文件所有的信息的數據結構。
針對套接字的系統數據結構:
1)、套接字API里有個函數socket,它就是用來創建一個套接字。套接字設計的總體思路是,單個系統調用就可以創建任何套接字,因為套接字是相當籠統的。一旦套接字創建后,應用程序還需要調用其他函數來指定具體細節。例如調用socket將創建一個新的描述符條目:
2)、雖然套接字的內部數據結構包含很多字段,但是系統創建套接字后,大多數字字段沒有填寫。應用程序創建套接字后在該套接字可以使用之前,必須調用其他的過程來填充這些字段。
3、基本的socket接口函數
服務器端先初始化/創建Socket,然后與端口綁定/綁定地址(bind),對端口進行監聽(listen),調用accept阻塞/等待連續,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求并處理請求,然后把回應數據發送給客戶端,客戶端讀取數據,最后關閉連接,一次交互結束。
3.1、socket函數
函數原型:
int socket(int protofamily, int type, int protocol);
返回值:
//返回sockfd sockfd是描述符,類似于open函數。
函數功能:
socket函數對應于普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用于創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
函數參數:
protofamily:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
3.2、bind()函數
函數功能:
bind()函數把一個地址族中的特定地址賦給socket,也可以說是綁定ip端口和socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
函數原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數參數:
- 函數的三個參數分別為:sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
- addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同,
- addrlen:對應的是地址的長度。
3.3、listen()、connect()函數
函數功能:
如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
函數原型:
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數參數:
listen函數的第一個參數即為要監聽的socket描述字,第二個參數為相應socket可以排隊的最大連接個數。socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。
connect函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度。客戶端通過調用connect函數來建立與TCP服務器的連接。成功返回0,若連接失敗則返回-1。
3.4、accept()函數
函數功能:
TCP服務器端依次調用socket()、bind()、listen()之后,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就向TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之后,就會調用accept()函數取接收請求,這樣連接就建立好了。之后就可以開始網絡I/O操作了,即類同于普通文件的讀寫I/O操作。
函數原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回連接connect_fd
函數參數:
sockfd:
參數sockfd就是上面解釋中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與服務器連接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。當然客戶不知道套接字這些細節,它只知道一個地址和一個端口號。
addr:
這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址,當然這個地址是通過某個地址結構來描述的,用戶應該知道這一個什么樣的地址結構。如果對客戶的地址不感興趣,那么可以把這個值設置為NULL。
len:
如同大家所認為的,它也是結果的參數,用來接受上述addr的結構的大小的,它指明addr結構所占有的字節個數。同樣的,它也可以被設置為NULL。
如果accept成功返回,則服務器與客戶已經正確建立連接了,此時服務器通過accept返回的套接字來完成與客戶的通信。
注意:
accept默認會阻塞進程,直到有一個客戶連接建立后返回,它返回的是一個新可用的套接字,這個套接字是連接套接字。
此時我們需要區分兩種套接字:
監聽套接字: 監聽套接字正如accept的參數sockfd,它是監聽套接字,在調用listen函數之后,是服務器開始調用socket()函數生成的,稱為監聽socket描述字(監聽套接字)
連接套接字:一個套接字會從主動連接的套接字變身為一個監聽套接字;而accept函數返回的是已連接socket描述字(一個連接套接字),它代表著一個網絡已經存在的點點連接。
一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
連接套接字socketfd_new 并沒有占用新的端口與客戶端通信,依然使用的是與監聽套接字socketfd一樣的端口號
3.5、recv()/send()函數
當然也可以使用其他函數來實現數據傳送,比如read和write。
3.5.1 send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
不論是客戶還是服務器應用程序都用send函數來向TCP連接的另一端發送數據。
客戶程序一般用send函數向服務器發送請求,而服務器則通常用send函數來向客戶程序發送應答。
第一個參數指定發送端套接字描述符;
第二個參數指明一個存放應用程序要發送數據的緩沖區;
第三個參數指明實際要發送的數據的字節數;
第四個參數一般置0。
3.5.2 recv
int recv( SOCKET s, char FAR *buf, int len, int flags );
不論是客戶還是服務器應用程序都用recv函數從TCP連接的另一端接收數據。
該函數的第一個參數指定接收端套接字描述符;
第二個參數指明一個緩沖區,該緩沖區用來存放recv函數接收到的數據;
第三個參數指明buf的長度;
第四個參數一般置0。
3.6、close()函數
函數功能:
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
函數原型:
#include <unistd.h>
int close(int fd);
close一個TCP socket的缺省行為時把該socket標記為以關閉,然后立即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。
注意:
close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。
4、Linux下Socket編程實例
我們在Xshell5中,開啟兩個會話,分別用來運行socket_server端、socket_client端。
4.1 編寫socket_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//第一步:導入Socket編程的標準庫
//這個標準庫:linux數據類型(size_t、time_t等等)
#include <sys/types.h>
//提供socket函數以及數據結構
#include <sys/socket.h>
//數據解構(sockaddr_in)
#include <netinet/in.h>
//IP地址的轉換函數
#include <arpa/inet.h>
//定義服務端
#define SERVER_PORT 9999
int main(){
//第二步:創建socket
//服務端的socket
int server_socket_fd;
//客戶端
int client_socket_fd;
//服務端網絡地址
struct sockaddr_in server_addr;
//客戶端網絡地址
struct sockaddr_in client_addr;
//初始化網絡地址
//參數一:傳變量的地址($server_addr)
//參數二:開始為止
//參數三:大小
//初始化服務端網絡地址
memset(&server_addr,0,sizeof(server_addr ));
//初始化客戶端網絡地址
//memset(&client_addr,0,sizeof(client_addr));
//設置服務端網絡地址-協議簇(sin_family)
//AF_INET:TCP/IP協議、UDP
//AF_ISO:ISO 協議
server_addr.sin_family = AF_INET;
//設置服務端IP地址(自動獲取系統默認的本機IP,自動分配)
server_addr.sin_addr.s_addr = INADDR_ANY;
//設置服務端端口
server_addr.sin_port = htons(SERVER_PORT);
//創建服務端socket
//參數一(family):通信域(例如:IPV4->PF_INET、IPV6等等......)
//參數二(type):通信類型(例如:TCP->SOCK_STREAM,UDP->SOCK_DGRAM等等......)
//參數三(protocol):指定使用的協議(一般情況下都是默認為0)
//默認為0就是使用系統默認的協議,系統支持什么我就就用什么
//TCP->IPPROTO_TCP
//UDP->IPPROTO_UDP
//SCTP->IPPROTO_SCTP
server_socket_fd = socket(PF_INET,SOCK_STREAM,0);
//判斷是否創建成功
if(server_socket_fd <0){
printf("create error!");
return 1;
}
printf("服務器創建成功!\n");
//服務端綁定地址
//參數一:服務端socket
//參數二:網絡地址
//參數三:數據類型大小
//socketaddr和sockaddr_in
bind(server_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//監聽客戶端連接請求(服務端監聽有沒有客戶端連接)
//參數一:監聽的服務器socket
//參數二:客戶端數量(未處理隊列數量)
listen(server_socket_fd,6);
//接收客戶端連接
//參數一(sockfd):服務端
//參數二(addr):客戶端
//參數三(addrlen):大小
socklen_t sin_size = sizeof(struct sockaddr_in);
//獲取一個客戶端
client_socket_fd= accept(server_socket_fd,(struct sockaddr*)&client_socket_fd,&sin_size);
//判斷客戶端是否連接成功
if(client_socket_fd < 0){
printf("連接失敗");
return 1;
}
//連接成功:讀取客戶端數據
//BUFSIZ:默認值
char buffer[BUFSIZ];
int len=0;
while(1){
//參數一:讀取客戶端數據(數據源)
//參數二:讀取到哪里(我們要讀取到緩沖區buffer)
//參數三:每次讀取多大BUFSIZ
//參數四:從哪里開始讀0
len = recv(client_socket_fd,buffer,BUFSIZ,0);
if(len > 0){
//說明讀取到了數據
printf("%s\n",buffer);
}
}
//關閉服務端和客戶端Socket
//參數一:關閉的源
//參數二:關閉的類型(設置權限)
//SHUT_RD:關閉讀(只允許寫,不允許讀)
//SHUT_WR:關閉寫(只允許讀,不允許寫)
//SHUT_RDWR:讀寫都關閉(書寫都不允許)
shutdown(client_socket_fd,SHUT_RDWR);
shutdown(server_socket_fd,SHUT_RDWR);
printf("server end.....\n");
getchar();
return 0;
}
4.2 執行socket_server.c
root@jdu4e00u53f7:/usr/kpioneer/pthread# gcc -c socket_server.c
root@jdu4e00u53f7:/usr/kpioneer/pthread# gcc -o socket_server socket_server.o
root@jdu4e00u53f7:/usr/kpioneer/pthread# ./socket_server
服務器創建成功!
4.3 編寫socket_client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//第一步:導入Socket編程的標準庫
//這個標準庫:linux數據類型(size_t、time_t等等......)
#include<sys/types.h>
//提供socket函數及數據結構
#include<sys/socket.h>
//數據結構(sockaddr_in)
#include<netinet/in.h>
//ip地址的轉換函數
#include<arpa/inet.h>
//定義服務器的端口號
#define SERVER_PORT 9999
int main(){
//客戶端socket
int client_socket_fd;
//服務端網絡地址
struct sockaddr_in server_addr;
//客戶端網絡地址
struct sockaddr_in client_addr;
//初始化網絡地址
//參數一:傳變量的地址($server_addr)
//參數二:開始位置
//參數三:大小
//初始化服務端網絡地址
memset(&server_addr,0,sizeof(server_addr ));
//AF_INET:TCP/IP協議、UDP
//AF_ISO:ISO 協議
server_addr.sin_family = AF_INET;
//設置服務端IP地址(自動獲取系統默認的本機IP,自動分配)
server_addr.sin_addr.s_addr = INADDR_ANY;
//設置服務端端口
server_addr.sin_port = htons(SERVER_PORT);
//創建客戶端
client_socket_fd = socket(PF_INET,SOCK_STREAM,0);
//判斷是否創建成功
if(client_socket_fd < 0){
printf("create error!!!");
return 1;
}
//連接服務器
//參數一:哪一個客戶端
//參數二:連接服務器地址
//參數三:地址大小
int con_result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(con_result<0){
printf("connect error!");
return -1;
}
printf("create Socket Client\n ");
//發送消息(向服務器發送內容)
char buffer[BUFSIZ] = "Hello, Socket Server!";
//參數一:指定客戶端
//參數二:指定緩沖區(沖那里數據讀取)
//參數三:實際讀取的大小strlen(buffer)(其實讀取到"\0"結束)
//參數四:從哪里開始讀取
send(client_socket_fd,buffer,strlen(buffer),0);
//關閉
shutdown(client_socket_fd,SHUT_RDWR);
printf("client--- end-----\n");
return 0;
}
4.4 執行socket_client.c
root@jdu4e00u53f7:/usr/kpioneer/pthread# gcc -o socket_client socket_client.o
root@jdu4e00u53f7:/usr/kpioneer/pthread# ./socket_client
create Socket Client
client--- end-----
4.5 再次查看socket_server
我們看到
服務器創建成功!
下多了一個打印語句Hello, Socket Server!
,程序運行成功。
5、Android下Socket編程實例(jni實現)
我們新建一個Java工程和一個Android工程,分別用來運行socket_server端、socket_client端。
5.1 Java工程端
5.1.1 編寫 SocketServer.java
package com.haocai;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) {
try{
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("192.168.90.221",9998));
System.out.println("服務器Start...");
while(true){
//獲取連接客戶端
Socket socket = serverSocket.accept();
//讀取內容
new ReaderThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class ReaderThread extends Thread{
BufferedReader bufferedReader;
public ReaderThread(Socket socket){
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
//循環讀取內容
String content = null;
while(true){
try {
while((content = bufferedReader.readLine())!=null){
System.out.println("接收到了客戶端:"+content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5.1.2 運行SocketServer
服務器Start...
5.2 Android工程端
5.2.1 編寫Java jni聲明
package com.haocai.socketclient;
public class SocketUtil {
public native void startClient(String serverIp,int serverPort);
static {
System.loadLibrary("socketlib");
}
}
5.2.2 編寫socket_client.c
#include"com_haocai_socketclient_SocketUtil.h"
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//第一步:導入Socket編程的標準庫
//這個標準庫:linux數據類型(size_t、time_t等等......)
#include<sys/types.h>
//提供socket函數及數據結構
#include<sys/socket.h>
//數據結構(sockaddr_in)
#include<netinet/in.h>
//ip地址的轉換函數
#include<arpa/inet.h>
#include <android/log.h>
#define LOG_TAG "socket_client"
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,FORMAT,##__VA_ARGS__);
#define LOGD(FORMAT,...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,FORMAT, ##__VA_ARGS__);
JNIEXPORT void JNICALL Java_com_haocai_socketclient_SocketUtil_startClient
(JNIEnv *env, jobject jobj, jstring server_ip_jstr, jint server_port){
const char* server_ip = (*env)->GetStringUTFChars(env, server_ip_jstr, NULL);
//客戶端socket
int client_socket_fd;
//服務端網絡地址
struct sockaddr_in server_addr;
//初始化網絡地址
//參數一:傳變量的地址($server_addr)
//參數二:開始位置
//參數三:大小
//初始化服務端網絡地址
memset(&server_addr,0,sizeof(server_addr));
//AF_INET:TCP/IP協議、UDP
//AF_ISO:ISO 協議
server_addr.sin_family = AF_INET;
//設置服務端IP地址(自動獲取系統默認的本機IP,自動分配)
server_addr.sin_addr.s_addr = inet_addr(server_ip);
//設置服務端端口
server_addr.sin_port = htons(server_port);
//創建客戶端
client_socket_fd = socket(PF_INET,SOCK_STREAM,0);
//判斷是否創建成功
if(client_socket_fd < 0){
LOGE("create error!");
return ;
}
//連接服務器
//參數一:哪一個客戶端
//參數二:連接服務器地址
//參數三:地址大小
int con_result = connect(client_socket_fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(con_result<0){
LOGE("connect error!");
return ;
}
//發送消息(向服務器發送內容)
char buffer[BUFSIZ] = "Hello Socket Server!";
//參數一:指定客戶端
//參數二:指定緩沖區(沖那里數據讀取)
//參數三:實際讀取的大小strlen(buffer)(其實讀取到"\0"結束)
//參數四:從哪里開始讀取
send(client_socket_fd,buffer,strlen(buffer),0);
//關閉
shutdown(client_socket_fd,SHUT_RDWR);
LOGI("client--- end-----");
(*env)->ReleaseStringUTFChars(env, server_ip_jstr, server_ip);
return ;
}
5.2.3 調用主程序MainActivity
package com.haocai.socketclient;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
public static final String SERVER_IP = "192.168.90.221";
public static final int SERVER_PORT = 9998;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void startSocket(View v){
new Thread(new Runnable() {
@Override
public void run() {
SocketUtil socketUtil = new SocketUtil();
socketUtil.startClient(SERVER_IP,SERVER_PORT);
}
}).start();
}
}
5.3 再次查看Java工程Log
接收到了客戶端:Hello Socket Server!