大綱
一.Socket簡介
二.BSD Socket編程準(zhǔn)備
- 1.地址
- 2.端口
- 3.網(wǎng)絡(luò)字節(jié)序
- 4.半相關(guān)與全相關(guān)
- 5.網(wǎng)絡(luò)編程模型
三.socket接口編程示例
四.使用select
五.使用kqueue
六.使用流
一.Socket簡介
在UNIX系統(tǒng)中,萬物皆文件(Everything is a file)。所有的IO操作都可以看作對文件的IO操作光酣,都遵循著這樣的操作模式:打開 -> 讀/寫 -> 關(guān)閉吩愧,打開操作(如open函數(shù))獲取“文件”使用權(quán),返回文件描述符棺妓,后繼的操作都通過這個文件描述符來進(jìn)行价涝。很多系統(tǒng)調(diào)用都依賴于文件描述符,它是一個無符號整數(shù),每一個用戶進(jìn)程都對應(yīng)著一個文件描述符表验烧,通過文件描述符就可以找到對應(yīng)文件的信息板驳。 在類UNIX平臺上,對于控制臺的標(biāo)準(zhǔn)輸入輸出以及標(biāo)準(zhǔn)錯誤輸出都有對應(yīng)的文件描述符碍拆,分別為0,1,2若治。它們定義在 unistd.h中
#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */
UNIX內(nèi)核加入TCP/IP協(xié)議的時候,便在系統(tǒng)中引入了一種新的IO操作感混,只不過由于網(wǎng)絡(luò)連接的不可靠性端幼,所以網(wǎng)絡(luò)IO比本地設(shè)備的IO復(fù)雜很多。這一系列的接口叫做BSD Socket API,當(dāng)初由伯克利大學(xué)研發(fā)弧满,最終成為網(wǎng)絡(luò)開發(fā)接口的標(biāo)準(zhǔn)婆跑。 網(wǎng)絡(luò)通信從本質(zhì)上講也是進(jìn)程間通信,只是這兩個進(jìn)程一般在網(wǎng)絡(luò)中不同計算機(jī)上庭呜。當(dāng)然Socket API其實(shí)也提供了專門用于本地IPC的使用方式:UNIX Domain Socket滑进,這個這里就不細(xì)說了。本文所講的Socket如無例外募谎,均是說的Internet Socket扶关。
在本地的進(jìn)程中,每一個進(jìn)程都可以通過PID來標(biāo)識数冬,對于網(wǎng)絡(luò)上的一個計算機(jī)中的進(jìn)程如何標(biāo)識呢节槐?網(wǎng)絡(luò)中的計算機(jī)可以通過一個IP地址進(jìn)行標(biāo)識,一個計算機(jī)中的某個進(jìn)程則可以通過一個無符號整數(shù)(端口號)來標(biāo)識吉执,所以一個網(wǎng)絡(luò)中的進(jìn)程可以通過IP地址+端口號的方式進(jìn)行標(biāo)識疯淫。
二.BSD Socket編程準(zhǔn)備
1.地址
在程序中,我們?nèi)绾伪4嬉粋€地址呢戳玫?在 <sys/socket.h>
中的sockaddr便是描述socket地址的結(jié)構(gòu)體類型.
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
為了方便設(shè)置用語網(wǎng)絡(luò)通信的socket地址熙掺,引入了sockaddr_in結(jié)構(gòu)體(對于UNIX Domain Socket則對應(yīng)sockaddr_un)
/*
* Socket address, internet style.
*/
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;//得是網(wǎng)絡(luò)字節(jié)序
struct in_addr sin_addr;//in_addr存在的原因則是歷史原因,其實(shí)質(zhì)是代表一個IP地址的32位整數(shù)
char sin_zero[8];//bzero之咕宿,純粹是為了兼容sockaddr
};
在實(shí)際編程的時候币绩,經(jīng)常需要將sockaddr_in強(qiáng)制轉(zhuǎn)換成sockaddr類型。
2.端口
說到端口我們經(jīng)常會聯(lián)想到硬件府阀,在網(wǎng)絡(luò)編程中的端口其實(shí)是一個標(biāo)識而已缆镣,或者說是系統(tǒng)的資源而已。系統(tǒng)提供了端口分配和管理的機(jī)制试浙。
3.網(wǎng)絡(luò)字節(jié)序
談網(wǎng)絡(luò)字節(jié)序(Endianness)之前我們先說說什么是字節(jié)序董瞻。字節(jié)序又叫端序,就是指計算機(jī)中存放 多字節(jié)數(shù)據(jù)的字節(jié)的順序。典型的就是數(shù)據(jù)存放在內(nèi)存中或者網(wǎng)絡(luò)傳輸時的字節(jié)的順序钠糊。常用的字節(jié)序有大端序(big-endian)挟秤,小端序(litle-endian,另還有不常見的混合序middle-endian)。不同的CPU可能會使用不同的字節(jié)序抄伍,如X86艘刚,PDP-11等處理器為小端序,Motorola 6800,PowerPC 970等使用的是大端序截珍。小端序是指低字節(jié)位存放在內(nèi)存地址的低端攀甚,高端序是指高位字節(jié)存放在內(nèi)存的低端。 舉個例子來說明什么是大端序和小端序: 比如一個4字節(jié)的整數(shù) 16進(jìn)制形式為 0x12345678岗喉,最左邊是高位秋度。
大端序
低位
高位
12
34
56
78
小端序
低位
高位
78
56
34
12
TCP/IP 各層協(xié)議將字節(jié)序使用的是大端序,我們把TCP/IP協(xié)議中使用的字節(jié)序稱之為網(wǎng)絡(luò)字節(jié)序沈堡。 編程的時候可以使用定義在sys/_endian.h中的相關(guān)的接口進(jìn)行本地字節(jié)序和網(wǎng)絡(luò)字節(jié)序的互轉(zhuǎn)静陈。
#define ntohs(x) __DARWIN_OSSwapInt16(x) // 16位整數(shù) 網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)主機(jī)字節(jié)序
#define htons(x) __DARWIN_OSSwapInt16(x) // 16位整數(shù) 主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序
#define ntohl(x) __DARWIN_OSSwapInt32(x) //32位整數(shù) 網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)主機(jī)字節(jié)序
#define htonl(x) __DARWIN_OSSwapInt32(x) //32位整數(shù) 主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序
以上聲明中 n代表netwrok燕雁, h代表host 诞丽,s代表short,l代表long
如果數(shù)據(jù)是單字節(jié)的話拐格,則其沒有字節(jié)序的說法了僧免。
4.半相關(guān)與全相關(guān)
半相關(guān)(half-association)是指一個三元組 (協(xié)議,本地IP地址,本地端口),通過這個三元組就可以唯一標(biāo)識一個網(wǎng)絡(luò)中的進(jìn)程,一般用于listening socket。但是實(shí)際進(jìn)行通信的過程捏浊,至少需要兩個進(jìn)程懂衩,且它們所使用的協(xié)議必須一致,所以一個完成的網(wǎng)絡(luò)通信至少需要一個五元組表示(協(xié)議,本地地址,本地端口,遠(yuǎn)端地址,遠(yuǎn)端端口)金踪,這樣的五元組叫做全相關(guān)浊洞。
5.網(wǎng)絡(luò)編程模型
網(wǎng)絡(luò)存在的本質(zhì)其實(shí)就是網(wǎng)絡(luò)中個體之間的在某個領(lǐng)域的信息存在不對等性,所以一般情況下總有一些個體為另一些個體提供服務(wù)胡岔。提供服務(wù)器的我們把它叫做服務(wù)器法希,接受服務(wù)的叫做客戶端。所以在網(wǎng)絡(luò)編程中靶瘸,也存在服務(wù)器端和客戶端之分苫亦。
三.BSD Socket編程詳解
下面的例子是一個簡單的一對一聊天的程序,分服務(wù)器和客戶端怨咪,且發(fā)送消息和接受消息次序固定屋剑。
Server端代碼
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int main (int argc, const char * argv[])
{
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;//Address families AF_INET互聯(lián)網(wǎng)地址簇
server_addr.sin_port = htons(11332);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
//創(chuàng)建socket
int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有連接
if (server_socket == -1) {
perror("socket error");
return 1;
}
//綁定socket:將創(chuàng)建的socket綁定到本地的IP地址和端口,此socket是半相關(guān)的诗眨,只是負(fù)責(zé)偵聽客戶端的連接請求唉匾,并不能用于和客戶端通信
int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bind_result == -1) {
perror("bind error");
return 1;
}
//listen偵聽 第一個參數(shù)是套接字,第二個參數(shù)為等待接受的連接的隊列的大小匠楚,在connect請求過來的時候,完成三次握手后先將連接放到這個隊列中巍膘,直到被accept處理卫病。如果這個隊列滿了,且有新的連接的時候典徘,對方可能會收到出錯信息蟀苛。
if (listen(server_socket, 5) == -1) {
perror("listen error");
return 1;
}
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
//返回的client_socket為一個全相關(guān)的socket,其中包含client的地址和端口信息逮诲,通過client_socket可以和客戶端進(jìn)行通信帜平。
if (client_socket == -1) {
perror("accept error");
return -1;
}
char recv_msg[1024];
char reply_msg[1024];
while (1) {
bzero(recv_msg, 1024);
bzero(reply_msg, 1024);
printf("reply:");
scanf("%s",reply_msg);
send(client_socket, reply_msg, 1024, 0);
long byte_num = recv(client_socket,recv_msg,1024,0);
recv_msg[byte_num] = '\0';
printf("client said:%s\n",recv_msg);
}
return 0;
}
Client端代碼
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int main (int argc, const char * argv[])
{
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(11332);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket error");
return 1;
}
char recv_msg[1024];
char reply_msg[1024];
if (connect(server_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0) {
//connect 成功之后,其實(shí)系統(tǒng)將你創(chuàng)建的socket綁定到一個系統(tǒng)分配的端口上梅鹦,且其為全相關(guān)裆甩,包含服務(wù)器端的信息,可以用來和服務(wù)器端進(jìn)行通信齐唆。
while (1) {
bzero(recv_msg, 1024);
bzero(reply_msg, 1024);
long byte_num = recv(server_socket,recv_msg,1024,0);
recv_msg[byte_num] = '\0';
printf("server said:%s\n",recv_msg);
printf("reply:");
scanf("%s",reply_msg);
if (send(server_socket, reply_msg, 1024, 0) == -1) {
perror("send error");
}
}
}
// insert code here...
printf("Hello, World!\n");
return 0;
}
上面的服務(wù)器端和客戶端連接成功之后打開的端口的情況是怎么樣的呢嗤栓? * 服務(wù)器端 ,存在一個用于listen的半相關(guān)的socket,一個用于和客戶端進(jìn)行通信的全相關(guān)的socket 四.使用select select這個系統(tǒng)調(diào)用,是一種多路復(fù)用IO方案蚁署,可以同時對多個文件描述符進(jìn)行監(jiān)控便脊,從而知道哪些文件描述符可讀,可寫或者出錯形用,不過select方法是阻塞的就轧,可以設(shè)定超時時間。 select使用的步驟如下:
- 1.創(chuàng)建一個fd_set變量(fd_set實(shí)為包含了一個整數(shù)數(shù)組的結(jié)構(gòu)體)田度,用來存放所有的待檢查的文件描述符
- 2.清空fd_set變量妒御,并將需要檢查的所有文件描述符加入fd_set
- 3.調(diào)用select。若返回-1镇饺,則說明出錯;返回0,則說明超時乎莉,返回正數(shù),則為發(fā)生狀態(tài)變化的文件描述符的個數(shù)
- 4.若select返回大于0,則依次查看哪些文件描述符變的可讀,并對它們進(jìn)行處理
- 5.返回步驟2惋啃,開始新一輪的檢測 若上面的聊天程序使用select進(jìn)行改寫哼鬓,則是下面這樣的
服務(wù)器端
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define BACKLOG 5 //完成三次握手但沒有accept的隊列的長度
#define CONCURRENT_MAX 8 //應(yīng)用層同時可以處理的連接
#define SERVER_PORT 11332
#define BUFFER_SIZE 1024
#define QUIT_CMD ".quit"
int client_fds[CONCURRENT_MAX];
int main (int argc, const char * argv[])
{
char input_msg[BUFFER_SIZE];
char recv_msg[BUFFER_SIZE];
//本地地址
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
//創(chuàng)建socket
int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock_fd == -1) {
perror("socket error");
return 1;
}
//綁定socket
int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bind_result == -1) {
perror("bind error");
return 1;
}
//listen
if (listen(server_sock_fd, BACKLOG) == -1) {
perror("listen error");
return 1;
}
//fd_set
fd_set server_fd_set;
int max_fd = -1;
struct timeval tv;
tv.tv_sec = 20;
tv.tv_usec = 0;
while (1) {
FD_ZERO(&server_fd_set);
//標(biāo)準(zhǔn)輸入
FD_SET(STDIN_FILENO, &server_fd_set);
if (max_fd < STDIN_FILENO) {
max_fd = STDIN_FILENO;
}
//服務(wù)器端socket
FD_SET(server_sock_fd, &server_fd_set);
if (max_fd < server_sock_fd) {
max_fd = server_sock_fd;
}
//客戶端連接
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
FD_SET(client_fds[i], &server_fd_set);
if (max_fd < client_fds[i]) {
max_fd = client_fds[i];
}
}
}
int ret = select(max_fd+1, &server_fd_set, NULL, NULL, &tv);
if (ret < 0) {
perror("select 出錯\n");
continue;
}else if(ret == 0){
printf("select 超時\n");
continue;
}else{
//ret為未狀態(tài)發(fā)生變化的文件描述符的個數(shù)
if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
//標(biāo)準(zhǔn)輸入
bzero(input_msg, BUFFER_SIZE);
fgets(input_msg, BUFFER_SIZE, stdin);
//輸入 ".quit" 則退出服務(wù)器
if (strcmp(input_msg, QUIT_CMD) == 0) {
exit(0);
}
for (int i=0; i<CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
send(client_fds[i], input_msg, BUFFER_SIZE, 0);
}
}
}
if (FD_ISSET(server_sock_fd, &server_fd_set)) {
//有新的連接請求
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
if (client_socket_fd > 0) {
int index = -1;
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i] == 0) {
index = i;
client_fds[i] = client_socket_fd;
break;
}
}
if (index >= 0) {
printf("新客戶端(%d)加入成功 %s:%d \n",index,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}else{
bzero(input_msg, BUFFER_SIZE);
strcpy(input_msg, "服務(wù)器加入的客戶端數(shù)達(dá)到最大值,無法加入!\n");
send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
printf("客戶端連接數(shù)達(dá)到最大值,新客戶端加入失敗 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}
}
}
for (int i = 0; i <CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
if (FD_ISSET(client_fds[i], &server_fd_set)) {
//處理某個客戶端過來的消息
bzero(recv_msg, BUFFER_SIZE);
long byte_num = recv(client_fds[i],recv_msg,BUFFER_SIZE,0);
if (byte_num > 0) {
if (byte_num > BUFFER_SIZE) {
byte_num = BUFFER_SIZE;
}
recv_msg[byte_num] = '\0';
printf("客戶端(%d):%s\n",i,recv_msg);
}else if(byte_num < 0){
printf("從客戶端(%d)接受消息出錯.\n",i);
}else{
FD_CLR(client_fds[i], &server_fd_set);
client_fds[i] = 0;
printf("客戶端(%d)退出了\n",i);
}
}
}
}
}
}
return 0;
}
客戶端
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024
int main (int argc, const char * argv[])
{
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(11332);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock_fd == -1) {
perror("socket error");
return 1;
}
char recv_msg[BUFFER_SIZE];
char input_msg[BUFFER_SIZE];
if (connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0) {
fd_set client_fd_set;
struct timeval tv;
tv.tv_sec = 20;
tv.tv_usec = 0;
while (1) {
FD_ZERO(&client_fd_set);
FD_SET(STDIN_FILENO, &client_fd_set);
FD_SET(server_sock_fd, &client_fd_set);
int ret = select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
if (ret < 0 ) {
printf("select 出錯!\n");
continue;
}else if(ret ==0){
printf("select 超時!\n");
continue;
}else{
if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
bzero(input_msg, BUFFER_SIZE);
fgets(input_msg, BUFFER_SIZE, stdin);
if (send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1) {
perror("發(fā)送消息出錯!\n");
}
}
if (FD_ISSET(server_sock_fd, &client_fd_set)) {
bzero(recv_msg, BUFFER_SIZE);
long byte_num = recv(server_sock_fd,recv_msg,BUFFER_SIZE,0);
if (byte_num > 0) {
if (byte_num > BUFFER_SIZE) {
byte_num = BUFFER_SIZE;
}
recv_msg[byte_num] = '\0';
printf("服務(wù)器:%s\n",recv_msg);
}else if(byte_num < 0){
printf("接受消息出錯!\n");
}else{
printf("服務(wù)器端退出!\n");
exit(0);
}
}
}
}
}
return 0;
}
當(dāng)然select也有其局限性边灭。當(dāng)fd_set中的文件描述符較少异希,或者大都數(shù)文件描述符都比較活躍的時候,select的效率還是不錯的绒瘦。Mac系統(tǒng)中已經(jīng)定義了fd_set 最大可以容納的文件描述符的個數(shù)為1024
//sys/_structs.h
#define __DARWIN_FD_SETSIZE 1024
/////////////////////////////////////////////
//Kernel.framework sys/select.h
#define FD_SETSIZE __DARWIN_FD_SETSIZE
每一次select 調(diào)用的時候称簿,都涉及到user space和kernel space的內(nèi)存拷貝,且會對fd_set中的所有文件描述符進(jìn)行遍歷惰帽,如果所有的文件描述符均不滿足憨降,且沒有超時,則當(dāng)前進(jìn)程便開始睡眠该酗,直到超時或者有文件描述符狀態(tài)發(fā)生變化授药。當(dāng)文件描述符數(shù)量較大的時候,將耗費(fèi)大量的CPU時間呜魄。所以后來有新的方案出現(xiàn)了悔叽,如windows2000引入的IOCP,Linux Kernel 2.6中成熟的epoll耕赘,F(xiàn)reeBSD4.x引入的kqueue骄蝇。
五.使用kqueue Mac是基于BSD的內(nèi)核
所使用的是kqueue(kernel event notification mechanism,詳細(xì)內(nèi)容可以Mac中 man 2 kqueue
)操骡,kqueue比select先進(jìn)的地方就在于使用事件觸發(fā)的機(jī)制,且其調(diào)用無需每次對所有的文件描述符進(jìn)行遍歷赚窃,返回的時候只返回需要處理的事件册招,而不像select中需要自己去一個個通過FD_ISSET檢查。 kqueue默認(rèn)的觸發(fā)方式是level 水平觸發(fā)勒极,可以通過設(shè)置event的flag為EV_CLEAR
使得這個事件變?yōu)檫呇赜|發(fā),可能epoll的觸發(fā)方式無法細(xì)化到單個event是掰,需要查證。 kqueue中涉及兩個系統(tǒng)調(diào)用辱匿,kqueue()和kevent()
- kqueue() 創(chuàng)建kernel級別的事件隊列键痛,并返回隊列的文件描述符
- kevent() 往事件隊列中加入訂閱事件,或者返回相關(guān)的事件數(shù)組 kqueue使用的流程一般如下:
- 創(chuàng)建kqueue
- 創(chuàng)建struct kevent變量(注意這里的kevent是結(jié)構(gòu)體類型名)匾七,可以通過EV_SET這個宏提供的快捷方式進(jìn)行創(chuàng)建
- 通過kevent系統(tǒng)調(diào)用將創(chuàng)建好的kevent結(jié)構(gòu)體變量加入到kqueue隊列中絮短,完成對指定文件描述符的事件的訂閱
- 通過kevent系統(tǒng)調(diào)用獲取滿足條件的事件隊列,并對每一個事件進(jìn)行處理
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/event.h>
#include <sys/types.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define BACKLOG 5 //完成三次握手但沒有accept的隊列的長度
#define CONCURRENT_MAX 8 //應(yīng)用層同時可以處理的連接
#define SERVER_PORT 11332
#define BUFFER_SIZE 1024
#define QUIT_CMD ".quit"
int client_fds[CONCURRENT_MAX];
struct kevent events[10];//CONCURRENT_MAX + 2
int main (int argc, const char * argv[])
{
char input_msg[BUFFER_SIZE];
char recv_msg[BUFFER_SIZE];
//本地地址
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
//創(chuàng)建socket
int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock_fd == -1) {
perror("socket error");
return 1;
}
//綁定socket
int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bind_result == -1) {
perror("bind error");
return 1;
}
//listen
if (listen(server_sock_fd, BACKLOG) == -1) {
perror("listen error");
return 1;
}
struct timespec timeout = {10,0};
//kqueue
int kq = kqueue();
if (kq == -1) {
perror("創(chuàng)建kqueue出錯!\n");
exit(1);
}
struct kevent event_change;
EV_SET(&event_change, STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &event_change, 1, NULL, 0, NULL);
EV_SET(&event_change, server_sock_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &event_change, 1, NULL, 0, NULL);
while (1) {
int ret = kevent(kq, NULL, 0, events, 10, &timeout);
if (ret < 0) {
printf("kevent 出錯!\n");
continue;
}else if(ret == 0){
printf("kenvent 超時!\n");
continue;
}else{
//ret > 0 返回事件放在events中
for (int i = 0; i < ret; i++) {
struct kevent current_event = events[i];
//kevent中的ident就是文件描述符
if (current_event.ident == STDIN_FILENO) {
//標(biāo)準(zhǔn)輸入
bzero(input_msg, BUFFER_SIZE);
fgets(input_msg, BUFFER_SIZE, stdin);
//輸入 ".quit" 則退出服務(wù)器
if (strcmp(input_msg, QUIT_CMD) == 0) {
exit(0);
}
for (int i=0; i<CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
send(client_fds[i], input_msg, BUFFER_SIZE, 0);
}
}
}else if(current_event.ident == server_sock_fd){
//有新的連接請求
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
if (client_socket_fd > 0) {
int index = -1;
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i] == 0) {
index = i;
client_fds[i] = client_socket_fd;
break;
}
}
if (index >= 0) {
EV_SET(&event_change, client_socket_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &event_change, 1, NULL, 0, NULL);
printf("新客戶端(fd = %d)加入成功 %s:%d \n",client_socket_fd,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}else{
bzero(input_msg, BUFFER_SIZE);
strcpy(input_msg, "服務(wù)器加入的客戶端數(shù)達(dá)到最大值,無法加入!\n");
send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
printf("客戶端連接數(shù)達(dá)到最大值昨忆,新客戶端加入失敗 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}
}
}else{
//處理某個客戶端過來的消息
bzero(recv_msg, BUFFER_SIZE);
long byte_num = recv((int)current_event.ident,recv_msg,BUFFER_SIZE,0);
if (byte_num > 0) {
if (byte_num > BUFFER_SIZE) {
byte_num = BUFFER_SIZE;
}
recv_msg[byte_num] = '\0';
printf("客戶端(fd = %d):%s\n",(int)current_event.ident,recv_msg);
}else if(byte_num < 0){
printf("從客戶端(fd = %d)接受消息出錯.\n",(int)current_event.ident);
}else{
EV_SET(&event_change, current_event.ident, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(kq, &event_change, 1, NULL, 0, NULL);
close((int)current_event.ident);
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i] == (int)current_event.ident) {
client_fds[i] = 0;
break;
}
}
printf("客戶端(fd = %d)退出了\n",(int)current_event.ident);
}
}
}
}
}
return 0;
}
其實(shí)kqueue的應(yīng)用場景非常的廣闊丁频,可以監(jiān)控文件系統(tǒng)中文件的變化(對文件變化的事件可以粒度非常的細(xì),具體可以查看kqueue的手冊),監(jiān)控系統(tǒng)進(jìn)程的生命周期席里。GCD的事件處理便是建立在kqueue之上的叔磷。
六.使用Streams
使用Objective-C的一大優(yōu)點(diǎn)便是面向?qū)ο缶幊蹋沟眠壿嫵橄蟮酶觾?yōu)美奖磁,更加符合人類思維改基。 一開始說過,無論是對于文件的操作或者對于網(wǎng)絡(luò)的操作咖为,本質(zhì)上都是IO操作寥裂,無非寫數(shù)據(jù)和讀數(shù)據(jù),可以對這種輸入輸出進(jìn)行抽象案疲,抽象成輸入流和輸出流封恰, 從輸入流中讀取數(shù)據(jù),往輸出流中寫數(shù)據(jù)褐啡。 Cocoa中的NSInputStream和NSOutputStream便是輸入流和輸出流的抽象诺舔,它們的實(shí)現(xiàn)分別基于CoreFoundation中的CFReadStream和CFWriteStream。 輸入輸出流對runloop有很好的支持备畦。 NSInputStream和CFReadStream以及NSOutputStream和CFWriteStream之間可以通過 "toll-free bridging"實(shí)現(xiàn)無縫的類型轉(zhuǎn)換低飒。 CoreFoundation中的CFStream提供了輸入輸出流和CFSocket綁定的函數(shù)。 這樣便可以通過輸入輸出流和遠(yuǎn)端進(jìn)行通信了懂盐。 首先通過XCode創(chuàng)建一個Foundation(C的也行褥赊,但是你得將main.c
改成main.m
)的命令行項目. 創(chuàng)建一個ChatServer的類,包含一個run的方法莉恼。在Cocoa的程序中有一點(diǎn)是和C語言不同的拌喉,你無需自己去寫一個死循環(huán)充當(dāng)runloop,框架本身就對runloop進(jìn)行了支持俐银,需要做的就是將事件源加入到當(dāng)前線程的runloop中尿背,然后啟動runloop。 所以在run方法中捶惜,創(chuàng)建好用于偵聽連接請求的socket田藐,socket有對應(yīng)的處理連接accept的回調(diào)函數(shù),以及把它封裝成runloop的輸入源吱七,加入到當(dāng)前runloop汽久。 我們還得從標(biāo)準(zhǔn)輸入獲取需要發(fā)送消息,所以使用了CFFileDescriptor踊餐,它是文件描述符的objc的封裝景醇,加入了runloop的支持,通過它可以將標(biāo)準(zhǔn)輸入以輸入源的方法加入到當(dāng)前runloop市袖,當(dāng)標(biāo)準(zhǔn)輸入緩沖區(qū)有數(shù)據(jù)可讀的時候啡直,設(shè)置好的回調(diào)函數(shù)便會被調(diào)用烁涌。 最后啟動runloop。
ChatServer中的run方法
- (BOOL)run:(NSError **)error{
BOOL successful = YES;
CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM,
IPPROTO_TCP,
kCFSocketAcceptCallBack,
(CFSocketCallBack)&SocketConnectionAcceptedCallBack,
&socketCtxt);
if (NULL == _socket) {
if (nil != error) {
*error = [[NSError alloc]
initWithDomain:ServerErrorDomain
code:kServerNoSocketsAvailable
userInfo:nil];
}
successful = NO;
}
if(YES == successful) {
// enable address reuse
int yes = 1;
setsockopt(CFSocketGetNative(_socket),
SOL_SOCKET, SO_REUSEADDR,
(void *)&yes, sizeof(yes));
uint8_t packetSize = 128;
setsockopt(CFSocketGetNative(_socket),
SOL_SOCKET, SO_SNDBUF,
(void *)&packetSize, sizeof(packetSize));
setsockopt(CFSocketGetNative(_socket),
SOL_SOCKET, SO_RCVBUF,
(void *)&packetSize, sizeof(packetSize));
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(CHAT_SERVER_PORT);
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
if (kCFSocketSuccess != CFSocketSetAddress(_socket, (CFDataRef)address4)) {
if (error) *error = [[NSError alloc]
initWithDomain:ServerErrorDomain
code:kServerCouldNotBindToIPv4Address
userInfo:nil];
if (_socket) CFRelease(_socket);
_socket = NULL;
successful = NO;
} else {
// now that the binding was successful, we get the port number
NSData *addr = [(NSData *)CFSocketCopyAddress(_socket) autorelease];
memcpy(&addr4, [addr bytes], [addr length]);
self.port = ntohs(addr4.sin_port);
// 將socket 輸入源加入到當(dāng)前的runloop
CFRunLoopRef cfrl = CFRunLoopGetCurrent();
CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
CFRunLoopAddSource(cfrl, source4, kCFRunLoopDefaultMode);
CFRelease(source4);
//標(biāo)準(zhǔn)輸入酒觅,當(dāng)在命令行中輸入時撮执,回調(diào)函數(shù)便會被調(diào)用
CFFileDescriptorContext context = {0,self,NULL,NULL,NULL};
CFFileDescriptorRef stdinFDRef = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, true, FileDescriptorCallBack, &context);
CFFileDescriptorEnableCallBacks(stdinFDRef,kCFFileDescriptorReadCallBack);
CFRunLoopSourceRef stdinSource = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, stdinFDRef, 0);
CFRunLoopAddSource(cfrl, stdinSource, kCFRunLoopDefaultMode);
CFRelease(stdinSource);
CFRelease(stdinFDRef);
CFRunLoopRun();
}
}
return successful;
}
當(dāng)有客戶端連接請求過來時, SocketConnectionAcceptedCallBack這個回調(diào)函數(shù)會被調(diào)用舷丹,根據(jù)新的全相關(guān)的socket抒钱,生成輸入輸出流,并設(shè)置輸入輸出流的delegate方法颜凯,將其添加到當(dāng)前的runloop谋币,這樣流中有數(shù)據(jù)過來的時候,delegate方法會被調(diào)用症概。 SocketConnectionAcceptedCallBack函數(shù)
static void SocketConnectionAcceptedCallBack(CFSocketRef socket,
CFSocketCallBackType type,
CFDataRef address,
const void *data, void *info) {
ChatServer *theChatServer = (ChatServer *)info;
if (kCFSocketAcceptCallBack == type) {
// 摘自kCFSocketAcceptCallBack的文檔蕾额,New connections will be automatically accepted and the callback is called with the data argument being a pointer to a CFSocketNativeHandle of the child socket. This callback is usable only with listening sockets.
CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
// create the read and write streams for the connection to the other process
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle,
&readStream, &writeStream);
if(NULL != readStream && NULL != writeStream) {
CFReadStreamSetProperty(readStream,
kCFStreamPropertyShouldCloseNativeSocket,
kCFBooleanTrue);
CFWriteStreamSetProperty(writeStream,
kCFStreamPropertyShouldCloseNativeSocket,
kCFBooleanTrue);
NSInputStream *inputStream = (NSInputStream *)readStream;//toll-free bridging
NSOutputStream *outputStream = (NSOutputStream *)writeStream;//toll-free bridging
inputStream.delegate = theChatServer;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
outputStream.delegate = theChatServer;
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream open];
Client *aClient = [[Client alloc] init];
aClient.inputStream = inputStream;
aClient.outputStream = outputStream;
aClient.sock_fd = nativeSocketHandle;
[theChatServer.clients setValue:aClient
forKey:[NSString stringWithFormat:@"%d",inputStream]];
NSLog(@"有新客戶端(sock_fd=%d)加入",nativeSocketHandle);
} else {
close(nativeSocketHandle);
}
if (readStream) CFRelease(readStream);
if (writeStream) CFRelease(writeStream);
}
}
當(dāng)客戶端有數(shù)據(jù)傳過來時,相應(yīng)的NSInputStream的delegate方法被調(diào)用
- (void) stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted: {
break;
}
case NSStreamEventHasBytesAvailable: {
Client *client = [self.clients objectForKey:[NSString stringWithFormat:@"%d",stream]];
NSMutableData *data = [NSMutableData data];
uint8_t *buf = calloc(128, sizeof(uint8_t));
NSUInteger len = 0;
while([(NSInputStream*)stream hasBytesAvailable]) {
len = [(NSInputStream*)stream read:buf maxLength:128];
if(len > 0) {
[data appendBytes:buf length:len];
}
}
free(buf);
if ([data length] == 0) {
//客戶端退出
NSLog(@"客戶端(sock_fd=%d)退出",client.sock_fd);
[self.clients removeObjectForKey:[NSString stringWithFormat:@"%d",stream]];
close(client.sock_fd);
}else{
NSLog(@"收到客戶端(sock_fd=%d)消息:%@",client.sock_fd,[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
}
break;
}
case NSStreamEventHasSpaceAvailable: {
break;
}
case NSStreamEventEndEncountered: {
break;
}
case NSStreamEventErrorOccurred: {
break;
}
default:
break;
}
}
當(dāng)在debug窗口中輸入內(nèi)容并回車時彼城,標(biāo)準(zhǔn)輸入緩沖區(qū)中便有數(shù)據(jù)了诅蝶,這個時候回調(diào)函數(shù)FileDescriptorCallBack將被調(diào)用,處理標(biāo)準(zhǔn)輸入募壕。
static void FileDescriptorCallBack(CFFileDescriptorRef f,
CFOptionFlags callBackTypes,
void *info){
int fd = CFFileDescriptorGetNativeDescriptor(f);
ChatServer *theChatServer = (ChatServer *)info;
if (fd == STDIN_FILENO) {
NSData *inputData = [[NSFileHandle fileHandleWithStandardInput] availableData];
NSString *inputString = [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"準(zhǔn)備發(fā)送消息:%@",inputString);
for (Client *client in [theChatServer.clients allValues]) {
[client.outputStream write:[inputData bytes] maxLength:[inputData length]];
}
//處理完數(shù)據(jù)之后必須重新Enable 回調(diào)函數(shù)
CFFileDescriptorEnableCallBacks(f,kCFFileDescriptorReadCallBack);
}
}
服務(wù)器端
創(chuàng)建Socket
將Socket和本地的地址端口綁定
開始進(jìn)行偵聽
握手成功调炬,接受請求,得到一個新的Socket舱馅,通過它可以和客戶端進(jìn)行通信
客戶端
創(chuàng)建一個Socket和服務(wù)器的地址并通過它們向服務(wù)器發(fā)送連接請求
連接成功缰泡,客戶端的Socket會綁定到系統(tǒng)分配的一個端口上,并可以通過它和服務(wù)器端進(jìn)行通信