转自 https://github.com/kejinlu/objc-doc/blob/master/Socket%E7%BC%96%E7%A8%8B.md
这篇文章绝对是无往不利的神奇,我写socket就是从模仿它的例子开始。堪称socket巅峰之作。
大纲
一.Socket简介
二.BSD Socket编程准备
1.地址
2.端口
3.网络字节序
4.半相关与全相关
5.网络编程模型
三.socket接口编程示例
四.使用select
五.使用kqueue
六.使用流
注:文档中设计涉及的代码也都在本人github目录下,分别为socketServer和socketClient.对应着各个分支。 分支
一.Socket简介
在UNIX系统中,万物皆文件(Everything is a file)。所有的IO操作都可以看作对文件的IO操作,都遵循着这样的操作模式:打开 -> 读/写 -> 关闭,打开操作(如open函数)获取“文件”使用权,返回文件描述符,后继的操作都通过这个文件描述符来进行。很多系统调用都依赖于文件描述符,它是一个无符号整数,每一个用户进程都对应着一个文件描述符表,通过文件描述符就可以找到对应文件的信息。 在类UNIX平台上,对于控制台的标准输入输出以及标准错误输出都有对应的文件描述符,分别为0,1,2。它们定义在 unistd.h中
在Mac系统中,可以通过Activity Monitor来查看某个进程打开的文件和端口。 已打开文件
UNIX内核加入TCP/IP协议的时候,便在系统中引入了一种新的IO操作,只不过由于网络连接的不可靠性,所以网络IO比本地设备的IO复杂很多。这一系列的接口叫做BSD Socket API,当初由伯克利大学研发,最终成为网络开发接口的标准。 网络通信从本质上讲也是进程间通信,只是这两个进程一般在网络中不同计算机上。当然Socket API其实也提供了专门用于本地IPC的使用方式:UNIX Domain Socket,这个这里就不细说了。本文所讲的Socket如无例外,均是说的Internet Socket。
在本地的进程中,每一个进程都可以通过PID来标识,对于网络上的一个计算机中的进程如何标识呢?网络中的计算机可以通过一个IP地址进行标识,一个计算机中的某个进程则可以通过一个无符号整数(端口号)来标识,所以一个网络中的进程可以通过IP地址+端口号的方式进行标识。
二.BSD Socket编程准备
1.地址
在程序中,我们如何保存一个地址呢?在
以上声明中 n代表netwrok, h代表host ,s代表short,l代表long
如果数据是单字节的话,则其没有字节序的说法了。
4.半相关与全相关
半相关(half-association)是指一个三元组 (协议,本地IP地址,本地端口),通过这个三元组就可以唯一标识一个网络中的进程,一般用于listening socket。但是实际进行通信的过程,至少需要两个进程,且它们所使用的协议必须一致,所以一个完成的网络通信至少需要一个五元组表示(协议,本地地址,本地端口,远端地址,远端端口),这样的五元组叫做全相关。
5.网络编程模型
网络存在的本质其实就是网络中个体之间的在某个领域的信息存在不对等性,所以一般情况下总有一些个体为另一些个体提供服务。提供服务器的我们把它叫做服务器,接受服务的叫做客户端。所以在网络编程中,也存在服务器端和客户端之分。
服务器端 客户端
创建Socket -
将Socket和本地的地址端口绑定 -
开始进行侦听 创建一个Socket和服务器的地址并通过它们向服务器发送连接请求
握手成功,接受请求,得到一个新的Socket,通过它可以和客户端进行通信 连接成功,客户端的Socket会绑定到系统分配的一个端口上,并可以通过它和服务器端进行通信
三.BSD Socket编程详解
下面的例子是一个简单的一对一聊天的程序,分服务器和客户端,且发送消息和接受消息次序固定。
Server端代码
/////////////////////////////////////////////
//Kernel.framework sys/select.h
每一次select 调用的时候,都涉及到user space和kernel space的内存拷贝,且会对fd_set中的所有文件描述符进行遍历,如果所有的文件描述符均不满足,且没有超时,则当前进程便开始睡眠,直到超时或者有文件描述符状态发生变化。当文件描述符数量较大的时候,将耗费大量的CPU时间。所以后来有新的方案出现了,如windows2000引入的IOCP,Linux Kernel 2.6中成熟的epoll,FreeBSD4.x引入的kqueue。
五.使用kqueue
Mac是基于BSD的内核,所使用的是kqueue(kernel event notification mechanism,详细内容可以Mac中 man 2 kqueue),kqueue比select先进的地方就在于使用事件触发的机制,且其调用无需每次对所有的文件描述符进行遍历,返回的时候只返回需要处理的事件,而不像select中需要自己去一个个通过FD_ISSET检查。
kqueue默认的触发方式是level 水平触发,可以通过设置event的flag为EV_CLEAR 使得这个事件变为边沿触发,可能epoll的触发方式无法细化到单个event,需要查证。
kqueue中涉及两个系统调用,kqueue()和kevent()
kqueue() 创建kernel级别的事件队列,并返回队列的文件描述符
kevent() 往事件队列中加入订阅事件,或者返回相关的事件数组
kqueue使用的流程一般如下:
创建kqueue
创建struct kevent变量(注意这里的kevent是结构体类型名),可以通过EV_SET这个宏提供的快捷方式进行创建
通过kevent系统调用将创建好的kevent结构体变量加入到kqueue队列中,完成对指定文件描述符的事件的订阅
通过kevent系统调用获取满足条件的事件队列,并对每一个事件进行处理
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);
//创建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(“创建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) {
//标准输入
bzero(input_msg, BUFFER_SIZE);
fgets(input_msg, BUFFER_SIZE, stdin);
//输入 “.quit” 则退出服务器
if (strcmp(input_msg, QUIT_CMD) == 0) {
exit(0);
}
for (int i=0; i