在internet网络的世界里,socket能够说是最重要的任务间通信的方式,尤为是当两个任务驻留在不一样的机器上须要经过网络介质链接。今天系统复习一下socket编程,由于本人已经有了基本的网络和操做系统的知识,直接跳过很基本的背景知识介绍了。我理解的socket就是抽象封装了传输层如下软硬件行为,为上层应用程序提供进程/线程间通讯管道。就是让应用开发人员不用管信息传输的过程,直接用socket API就OK了。贴个TCP的socket示意图体会如下。php
网上找了些写的不错的教程研究一下,着重参考The Tenouk's Linux Socket (network) programming tutorial和socket programming。重点就socket connection创建、通讯过程和高并发模式作一下深刻分析。html
udp和TCP socket通讯过程基本上是同样的,只是调用api时传入的配置不同,以TCP client/server模型为例子看一下整个过程。linux
socket: establish socket interface
gethostname: obtain hostname of system
gethostbyname: returns a structure of type hostent for the given host name
bind: bind a name to a socket
listen: listen for connections on a socket
accept: accept a connection on a socket
connect: initiate a connection on a socket
setsockopt: set a particular socket option for the specified socket.
close: close a file descriptor
shutdown: shut down part of a full-duplex connection编程
1. socket()api
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); - 参数说明 domain: 设定socket双方通讯协议域,是本地/internet ip4 or ip6 Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) type: 设定socket的类型,经常使用的有 SOCK_STREAM - 通常对应TCP、sctp SOCK_DGRAM - 通常对应UDP SOCK_RAW - protocol: 设定通讯使用的传输层协议 经常使用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,能够设置为0,系统本身选定。注意protocol和type不是随意组合的。
socket() API是在glibc中实现的,该函数又调用到了kernel的sys_socket(),调用链以下。网络
详细的kernel实现我没有去读,大致上这样理解。调用socket()会在内核空间中分配内存而后保存相关的配置。同时会把这块kernel的内存与文件系统关联,之后即可以经过filehandle来访问修改这块配置或者read/write socket。操做socket就像操做file同样,应了那句unix一切皆file。提示系统的最大filehandle数是有限制的,/proc/sys/fs/file-max设置了最大可用filehandle数。固然这是个linux的配置,能够更改,方法参见Increasing the number of open file descriptors,有人作到过1.6 million connection。并发
2. bind()app
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明 sockfd:以前socket()得到的file handle addr:绑定地址,可能为本机IP地址或本地文件路径 addrlen:地址长度 功能说明 bind()设置socket通讯的地址,若是为INADDR_ANY则表示server会监听本机上全部的interface,若是为127.0.0.1则表示监听本地的process通讯(外面的process也接不进啊)。
3. listen()dom
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 参数说明 sockfd:以前socket()得到的file handle backlog:设置server能够同时接收的最大连接数,server端会有个处理connection的queue,listen设置这个queue的长度。 功能说明 listen()只用于server端,设置接收queue的长度。若是queue满了,server端能够丢弃新到的connection或者回复客户端ECONNREFUSED。
4. accept()socket
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数说明: addr:对端地址 addrlen:地址长度 功能说明: accept()从queue中拿出第一个pending的connection,新建一个socket并返回。 新建的socket咱们叫connected socket,区别于前面的listening socket。 connected socket用来server跟client的后续数据交互,listening socket继续waiting for new connection。 当queue里没有connection时,若是socket经过fcntl()设置为 O_NONBLOCK,accept()不会block,不然通常会block。
疑问:kernel是如何区分listening socket和connected socket的呢??虽然两者的五元组是不同的,kernel如何知道经过哪一个socket跟APP交互?经过解析内容,是SYN仍是数据?暂时存疑。
5. connect()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明: sockfd: socket的标示filehandle addr:server端地址 addrlen:地址长度 功能说明: connect()用于双方链接的创建。 对于TCP链接,connect()实际发起了TCP三次握手,connect成功返回后TCP链接就创建了。 对于UDP,因为UDP是无链接的,connect()能够用来指定要通讯的对端地址,后续发数据send()就不须要填地址了。 固然UDP也能够不使用connect(),socket()创建后,在sendto()中指定对端地址。
这是TCP server代码例子,server收到client的任何数据后再回返给client。主进程负责accept()新进的connection并建立子进程,子进程负责跟client通讯。
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <unistd.h> #define MAXLINE 4096 /*max text line length*/ #define SERV_PORT 3000 /*port*/ #define LISTENQ 8 /*maximum number of client connections */ int main (int argc, char **argv) { int listenfd, connfd, n; socklen_t clilen; char buf[MAXLINE]; struct sockaddr_in cliaddr, servaddr; //creation of the socket listenfd = socket (AF_INET, SOCK_STREAM, 0); //preparation of the socket address servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // bind address bind (listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); // connection queue size 8 listen (listenfd, LISTENQ); printf("%s\n","Server running...waiting for connections."); while(1) { clilen = sizeof(cliaddr); connfd = accept (listenfd, (struct sockaddr *) &cliaddr, &clilen); printf("%s\n","Received request..."); if (!fork()) { // this is the child process close(listenfd); // child doesn't need the listener while ( (n = recv(connfd, buf, MAXLINE,0)) > 0) { printf("%s","String received from and resent to the client:"); puts(buf); send(connfd, buf, n, 0); if (n < 0) { perror("Read error"); exit(1); } } close(connfd); exit(0); } } //close listening socket close (listenfd);
}
TCP端代码,单进程。client与server创建连接后,从标准输入获得数据发给server并等待server的回传数据并打印输出,而后等待标准输入...
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #define MAXLINE 4096 /*max text line length*/ #define SERV_PORT 3000 /*port*/ int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; char sendline[MAXLINE], recvline[MAXLINE]; //basic check of the arguments if (argc !=2) { perror("Usage: TCPClient <IP address of the server"); exit(1); } //Create a socket for the client //If sockfd<0 there was an error in the creation of the socket if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <0) { perror("Problem in creating the socket"); exit(2); } //Creation of the socket memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr= inet_addr(argv[1]); servaddr.sin_port = htons(SERV_PORT); //convert to big-endian order //Connection of the client to the socket if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<0) { perror("Problem in connecting to the server"); exit(3); } while (fgets(sendline, MAXLINE, stdin) != NULL) { send(sockfd, sendline, strlen(sendline), 0); if (recv(sockfd, recvline, MAXLINE,0) == 0){ //error: server terminated prematurely perror("The server terminated prematurely"); exit(4); } printf("%s", "String received from the server: "); fputs(recvline, stdout); } exit(0); }
上面举的server的例子是用多进程来实现并发,固然还有其余比较高效的作法,好比IO复用。select和epoll是IO复用经常使用的系统调用,详细分析一下。
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); //fd_set类型示意 typedef struct { unsigned long fds_bits[1024 / 64]; // 8bytes*16=128bytes } fd_set; 参数说明: readfds: 要监控可读的sockets集合,看是否可读 writefds:要监控可写的sockets集合,看是否可写 exceptfds:要监控发生exception的sockets集合,看是否有exception nfds:上面三个sockets集合中最大的filehandle+1 timeout:阻塞的时间,0表示不阻塞,null表示无限阻塞 功能说明: 调用select()实践上是往kernel注册3组sockets监控集合,任何一个或多个sockets ready(状态跳变,不可读变可读 or 不可写变可写 or exception发生), 函数就会返回,不然一直block直到超时。 返回值>0表示ready的sockets个数,0表示超时,-1表示error。
epoll由3个函数协调完成,把整个过程分红了建立,配置,监控三步。
step1 建立epoll实体
#include <sys/epoll.h> int epoll_create(int size); 参数说明: size:随便给个>0的数值,如今系统不care了。 功能说明: epoll_create()在kernel内部分配了一块内存并关联到文件系统,函数调用成功会返回一个file handle来标识这块内存。 #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
Step2 配置监控的socket集合
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 参数说明: epfd:前面epoll_create()建立实体的标识 op:操做符,EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL fd:要监控的socket对应的file handle event:要监控的事件链表 功能说明: epoll_ctl()配置要对哪一个socket作什么样的事件监控。
step3 监控sockets
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 参数说明: epfd:epoll实体filehandle标识 events:指示发生的事情。application分配一块内存用event指针来指向,epoll_wait()调用时kernel将发生的事件存入event这块内存。 maxevents:最大可接收多少event timeout:超时时间,0表示当即返回,函数不block,-1表示无限block。 功能说明: epoll_wait()真正开始监控以前设置好的sockets集合。若是有事件发生,经过事件链表的方式返回给application。
有了上面的API,咱们能够比较直观的比较select和epoll的特色
select的memory copy比epoll多。
select每次调用都要有用户空间到kernel空间的内存copy,把全部要监控配置copy到内核。
epoll只须要epoll_ctl配置的时候copy,并且是增量copy,epoll_wait没有用户空间到内核的copy
select函数调用返回后的处理比epoll低效
select()返回给application有几件事情发生了,可是没说是谁有事情,application还得挨个遍历过去,看看谁有啥事
epoll_wait()返回给application更多的信息,谁发生了什么事都通知给application了,application直接处理这些事件就好了,不须要遍历
select相比epoll有处理socket数量的限制
select内核限定了1024最大的filehandle数,若是要修改须要编译内核
epoll没有固定的限制,能够达到系统最大filehandle数
小结一下二者的对比,一般能够看到epoll的效率更高,尤为是在大量socket并发的时候。有人说在少许sockets,好比10多个之内,select要有优点,我没有验证过。不过这么少的并发用哪一个都行,不会差异太大。
The Tenouk's Linux Socket (network) programming tutorial
Beej's Guide to Network Programming
socket programming
linux内核中socket的建立过程源码分析
how-to-use-epoll-a-complete-example-in-c
epoll manual
select manual