在internet网络的世界里,socket可以说是最重要的任务间通讯的方式,尤其是当两个任务驻留在不同的机器上需要通过网络介质连接。今天系统复习一下socket编程,因为本人已经有了基本的网络和操作系统的知识,直接跳过很基本的背景知识介绍了。我理解的socket就是抽象封装了传输层以下软硬件行为,为上层应用程序提供进程/线程间通信管道。就是让应用开发人员不用管信息传输的过程,直接用socket API就OK了。贴个TCP的socket示意图体会以下。
网上找了些写的不错的教程研究一下,着重参考The Tenouk's Linux Socket (network) programming tutorial和socket programming。重点就socket connection建立、通信过程和高并发模式做一下深入分析。
Socket通信过程和API全解析
udp和TCP socket通信过程基本上是一样的,只是调用api时传入的配置不一样,以TCP client/server模型为例子看一下整个过程。
socket API
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()
#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()
#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()
#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()
#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端
这是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端
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);
}
高并发socket -- select vs epoll
上面举的server的例子是用多进程来实现并发,当然还有其他比较高效的做法,比如IO复用。select和epoll是IO复用常用的系统调用,详细分析一下。
select API
#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 API
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。
对比select和epoll
有了上面的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