socket这个词可以表示很多概念,在TCP/IP协议中“IP地址 + TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP + 端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么两个socket组成的socket pair就唯一标识一个连接。
预备知识
网络字节序:内存中多字节数据相对于内存地址有大端小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,所以发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接收到的字节按内存从低到高的顺序保存,因此网络数据流的地址应该规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定网络数据流应该采用大端字节序,即低地址高字节。所以发送主机和接收主机是小段字节序的在发送和接收之前需要做字节序的转换。
为了使网络程序具有可移植性可以调用以下函数进行网络字节数的转换。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
socket地址数据类型及相关函数
sockaddr数据结构
IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有空指针类型这些函数的参数都⽤用struct sockaddr 类型表示,在传递参数之前要强制类型转换一下。
本次只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表⽰示32位的IP 地址。但是我们通常⽤用点分十进制的字符串表示IP 地址,以下函数可以在字符串表⽰示 和in_addr表⽰示之间转换。也就是说可以将字符串转换成in_addr类型,也可以将本地字节转换成网络字节。相反由同样有从网络转换到本地的函数具体的用法我们下面的代码中来看。
#include
#include
#include
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in)
int socket(int family, int type, int protocol)
family :指定协议的类型本次选择AF_INET(IPv4协议)。
type:网络数据类型,TCP是面向字节流的—SOCK_STREAM.
protocol:前两个参数一般确定了协议类型通常传0.
返回值:成功返回套接字符。
失败返回-1设置相关错误码。
int bind(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
sockfd : socket函数成功时候返回的套接字描述符。
servaddr :服务器的IP和端口。
addrlen : 长度(sizeof(servaddr))。
返回值:成功返回0
失败返回-1,并设置相关错误码。
int listen(int sockfd, int backlog)
sockfd: socket函数成功时候返回的套接字描述符。
backlog : 内核中套接字排队的最大个数。
返回值:成功返回0
失败返回-1,并设置相关错误码。
int accept(int sockfd, const struct sockaddr *servaddr, socklen_t *addrlen)
sockfd : socket函数成功时候返回的套接字描述符。
servaddr : 输出型参数,客户端的ip和端口。
addrlen : 长度(sizeof(servaddr))。
返回值:成功:从监听套接字返回已连接套接字
失败:失败返回-1,并设置相关错误码。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
sockfd:函数返回的套接字描述符
servaddr :服务器的IP和端口
addrlen : 长度(sizeof(servaddr))。
返回值:成功返回0
失败返回-1,并设置相关错误码
server.c
#include
#include
#include
#include
#include
#include
#include
#include
static usage(const char* proc)
{
printf("Usage:%s[local-ip][local-port]\n",proc);
}
static int start_up(const char *local_ip,int local_port)
{
//1.create sock
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
close(sock);
exit(1);
}
//2,createbind
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(local_port);
local.sin_addr.s_addr = inet_addr(local_ip);
if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0)
{
perror("bind");
close(sock);
exit(2);
}
//3.listen
if(listen(sock,10) < 0)
{
perror("listen");
close(sock);
exit(3);
}
return sock;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
}
int sock = start_up(argv[1],atoi(argv[2]));
struct sockaddr_in client;
socklen_t len = sizeof(client);
while(1)
{
int new_sock = accept(sock,(struct sockaddr*)&client, &len);
if(new_sock < 0)
{
perror("accept");
close(sock);
return 1;
}
printf("client_ip:%s client_port:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
char buf[1024];
while(1)
{
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client say# %s",buf);
}
else if(s == 0)
{
printf("client quit!\n");
break;
}
write(new_sock,buf, strlen(buf));
}
}
close(sock);
return 0;
}
client.c
#include
#include
#include
#include
#include
#include
#include
#include
static void usage(const char* proc)
{
printf("Usage:%s [server-ip] [server-port]",proc);
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr *)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
while(1)
{
printf("please Entry#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)//read success
{
buf[s] = 0;
}
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);
if(_s > 0)
{
buf[_s - 1] = 0;
printf("server echo# %s\n",buf);
}
}
close(sock);
return 0;
}
上述代码只可以处理单个用户,为了可以处理多个用户请求我们可以编写多进程或者多线程的TCP套接字。完整代码如下。
多进程TCPsocket
https://coding.net/u/Hyacinth_Dy/p/MyCode/git/blob/master/%E5%A4%9A%E8%BF%9B%E7%A8%8BTCP%E5%A5%97%E6%8E%A5%E5%AD%97多线程TCPsocket
https://coding.net/u/Hyacinth_Dy/p/MyCode/git/blob/master/%E5%A4%9A%E7%BA%BF%E7%A8%8BTcp%E5%A5%97%E6%8E%A5%E5%AD%97
对于上述代码就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即在性能上并不是合理的选择,因此我们需要提高代码的性能。下面介绍三种常用的高性能套接字编程方法。
select函数预备知识
select函数介绍
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
maxfdp : 需要监视的最大文件描述符加1。
readfds、writefds、errorfds:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
timeout:等待时间,这个时间内,需要监视的描述符没有事件
发⽣生则函数返回,返回值为0。设为NULL 表示阻塞式等待,一直等到有事件就绪,函数才会返回,0表示非阻塞式等待,没有事件就立即返回,大于0表示等待的时间。
返回值:大于0表示就绪时间的个数,等于0表示timeout等待时间到了,小于0表示调用失败。
select函数原理
select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这⾥里等待,直到被监视的文件句柄有一个或多个发⽣生了状态改变。关于文件句柄,其实就是⼀一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
1.我们通常需要额外定义一个数组来保存需要监视的文件描述符,并将其他没有保存描述符的位置初始化为一个特定值,一般为-1,这样方便我们遍历数组,判断对应的文件描述符是否发生了相应的事件。
2.采用上述的宏操作FD_SET(int fd,fd_set*set)遍历数组将关心的文件描述符设置到对应的事件集合里。并且每次调用之前都需要遍历数组,设置文件描述符。
3.调用select函数等待所关心的文件描述符。有文件描述符上的事件就绪后select函数返回,没有事件就绪的文件描述符在文件描述符集合中对应的位置会被置为0,这就是上述第二步的原因。
4.select 返回值大于0表示就绪的文件描述符的个数,0表示等待时间到了,小于0表示调用失败,因此我们可以遍历数组采用FD_ISSET(int fd,fd_set *set)判断哪个文件描述符上的事件就绪,然后执行相应的操作。
采用select的tcp socket实现代码。
#include
#include
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int array[4096];
static int start_up(const char* _ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(sock,10) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return -1;
}
int listensock = start_up(argv[1],atoi(argv[2]));
int maxfd = 0;
fd_set rfds;
fd_set wfds;
array[0] = listensock;
int i = 1;
int array_size = sizeof(array)/sizeof(array[0]);
for(; i < array_size;i++)
{
array[i] = -1;
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0;i < array_size;++i)
{
if(array[i] > 0)
{
FD_SET(array[i],&rfds);
FD_SET(array[i],&wfds);
if(array[i] > maxfd)
{
maxfd = array[i];
}
}
}
switch(select(maxfd + 1,&rfds,&wfds,NULL,NULL))
{
case 0:
{
printf("timeout\n");
break;
}
case -1:
{
perror("select");
break;
}
default:
{
int j = 0;
for(; j < array_size; ++j)
{
if(j == 0 && FD_ISSET(array[j],&rfds))
{
//listensock happened read events
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listensock,(struct sockaddr*)&client,&len);
if(new_sock < 0)//accept failed
{
perror("accept");
continue;
}
else//accept success
{
printf("get a new client%s\n",inet_ntoa(client.sin_addr));
fflush(stdout);
int k = 1;
for(; k < array_size;++k)
{
if(array[k] < 0)
{
array[k] = new_sock;
if(new_sock > maxfd)
maxfd = new_sock;
break;
}
}
if(k == array_size)
{
close(new_sock);
}
}
}//j == 0
else if(j != 0 && FD_ISSET(array[j], &rfds))
{
//new_sock happend read events
char buf[1024];
ssize_t s = read(array[j],buf,sizeof(buf) - 1);
if(s > 0)//read success
{
buf[s] = 0;
printf("clientsay#%s\n",buf);
if(FD_ISSET(array[j],&wfds))
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\nyingying beautiful
\r\n";
write(array[j],msg,strlen(msg));
}
}
else if(0 == s)
{
printf("client quit!\n");
close(array[j]);
array[j] = -1;
}
else
{
perror("read");
close(array[j]);
array[j] = -1;
}
}//else j != 0
}
break;
}
}
}
return 0;
}
client端同上面tcpsocket端相同。
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。
revents : 关心的事件就绪时 revents会被设置成上述对应的事件,除此之外还可能设置为如下内容。
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
#include
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
参数介绍:
fds : 对应上述介绍的结构体指针
nfds : 标记数组中结构体元素的总个数。
timeout : 超时时间 ,等于0表示非阻塞式等待,小于0表示阻塞式等待,大于0表示等待的时间。
返回值:
成功时返回fds数组中事件就绪的文件描述符的个数
返回0表示超时时间到了。
返回-1表示调用失败,对应的错误码会被设置。
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
poll函数实现原理
(1)将需要关心的文件描述符放进fds数组中
(2)调用poll函数
(3)函数成功返回后根据返回值遍历fds数组,将关心的事件与结构体中的revents相与判断事件是否就绪。
(4)事件就绪执行相关操作。
poll实现tcpsocket代码
#include
#include
#include
#include
#include
#include
#include
#include
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(sock,10) < 0)
{
perror("listen");
return 4;
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
struct pollfd peerfd[1024];
peerfd[0].fd = sock;
peerfd[0].events = POLLIN;
int nfds = 1;
int ret;
int maxsize = sizeof(peerfd)/sizeof(peerfd[0]);
int i = 1;
int timeout = -1;
for(; i < maxsize; ++i)
{
peerfd[i].fd = -1;
}
while(1)
{
switch(ret = poll(peerfd,nfds,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("poll");
break;
default:
{
if(peerfd[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(sock,\
(struct sockaddr*)&client,&len);
printf("accept finish %d\n",new_sock);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a new client\n");
int j = 1;
for(; j < maxsize; ++j)
{
if(peerfd[j].fd < 0)
{
peerfd[j].fd = new_sock;
break;
}
}
if(j == maxsize)
{
printf("to many clients...\n");
close(new_sock);
}
peerfd[j].events = POLLIN;
if(j + 1 > nfds)
nfds = j + 1;
}
for(i = 1;i < nfds;++i)
{
if(peerfd[i].revents & POLLIN)
{
printf("read ready\n");
char buf[1024];
ssize_t s = read(peerfd[i].fd,buf, \
sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
fflush(stdout);
peerfd[i].events = POLLOUT;
}
else if(s <= 0)
{
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//i != 0
else if(peerfd[i].revents & POLLOUT)
{
char *msg = "HTTP/1.0 200 OK \
<\r\n\r\n \
yingying beautiful \
\r\n";
write(peerfd[i].fd,msg,strlen(msg));
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//for
}//default
break;
}
}
return 0;
}
客户端同上。
epoll函数预备知识
epoll函数是多路复用IO接口select和poll函数的增强版本。显著减少程序在大量并发连接中只有少量活跃的情况下CPU利用率,他不会复用文件描述符集合来传递结果,而迫使开发者每次等待事件之前都必须重新设置要等待的文件描述符集合,另外就是获取事件时无需遍历整个文件描述符集合,只需要遍历被内核异步唤醒加入ready队列的描述符集合就行了 。
epoll函数相关系统调用
int epoll_create(int size);
生成一个epoll函数专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。 size 就是你在这个 Epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );
控制文件描述符上的事件,包括注册,删除,修改等操作。
epfd : epoll的专用描述符。
op : 相关操作,通常用以下宏来表示
event : 通知内核需要监听的事件,
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除⼀一个fd;
fd : 需要监听的事件。结构体格式如下:
events的合法参数如下
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这⾥里应该表⽰示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于⽔水平触发(Level
Triggered)来说的。
EPOLLONESHOT:只监听⼀一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加⼊入到EPOLL队列里。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd : epoll特有的文件描述符
events :从内核中的就绪队列中拷贝出就绪的文件描述符。不可以是空指针,内核只负责将数据拷贝到这里,不会为我们开辟空间。
maxevent : 高速内核events有多大,一般不能超过epoll_create传递的size,
timeout : 函数超时时间,0表示非阻塞式等待,-1表示阻塞式等待,函数返回0表示已经超时。
- epoll函数底层实现过程
首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次
- epoll实现tcpsocket代码
#include
#include
#include
#include
#include
#include
#include
#include
static Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock,10)< 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
int epollfd = epoll_create(256);
if(epollfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sock;
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock,&ev) < 0)
{
perror("epoll_ctl");
return 6;
}
int evnums = 0;//epoll_wait return val
struct epoll_event evs[64];
int timeout = -1;
while(1)
{
switch(evnums = epoll_wait(epollfd,evs,64,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("epoll_wait");
break;
default:
{
int i = 0;
for(; i < evnums; ++i)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if(evs[i].data.fd == sock \
&& evs[i].events & EPOLLIN)
{
int new_sock = accept(sock, \
(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
continue;
}//if accept failed
else
{
printf("Get a new client[%s]\n", \
inet_ntoa(client.sin_addr));
ev.data.fd = new_sock;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,\
new_sock,&ev);
}//accept success
}//if fd == sock
else if(evs[i].data.fd != sock && \
evs[i].events & EPOLLIN)
{
char buf[1024];
ssize_t s = read(evs[i].data.fd,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
ev.data.fd = evs[i].data.fd;
ev.events = EPOLLOUT;
epoll_ctl(epollfd,EPOLL_CTL_MOD, \
evs[i].data.fd,&ev);
}//s > 0
else
{
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}
}//fd != sock
else if(evs[i].data.fd != sock \
&& evs[i].events & EPOLLOUT)
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\nyingying beautiful
\r\n";
write(evs[i].data.fd,msg,strlen(msg));
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}//EPOLLOUT
else
{
}
}//for
}//default
break;
}//switch
}//while
return 0;
}