一般的简单网络编程就是创建一个 server 和 一个 client,然后分别send&recv 数据:
如果是针对一对一的连接(即一个服务器一个客户端),则如下代码:
在 linux 中网络编程需要用到这么一些头文件:
#include "sys/socket.h"
#include "arpa/inet.h"
#include "string.h"
#include "unistd.h"
#include "netinet/tcp.h"
#include "errno.h"
创建一个 server,绑定地址,监听等:下面这段代码还没有 accept 客户端的连接,因为 accept 的调用是阻塞的,如果没有连接进来,程序会一直阻塞在 accept 调用的地方。关于如何不阻塞地 accept,见下文的 serverPoll 调用。
bool TcpServer::createServer(int port_num)
{
int rc;
bool rtn;
const int reuse_addr = 1;
socklen_t addrSize = 0;
// 由socke系统调用创建的描述符需要保存起来,将这个 描述符 绑定到一个网络地址之后,对这个 描述符 的操作就是对这个服务器的操作!
rc = socket(AF_INET, SOCK_STREAM, 0);
if (-1 != rc)
{
this->setSrvrHandle(rc); // 保存这个描述符,用于后续操作。
// 避免出现:"address already in use"
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))
// Initialize address data structure
// sockaddr_ 是一个 sockaddr_in 结构体,也需要作为成员变量一直保存.
memset(&this->sockaddr_, 0, sizeof(this->sockaddr_));
this->sockaddr_.sin_family = AF_INET;
this->sockaddr_.sin_addr.s_addr = INADDR_ANY;
this->sockaddr_.sin_port = HTONS(port_num);
addrSize = sizeof(this->sockaddr_);
rc = bind(this->getSrvrHandle(), (sockaddr *)&(this->sockaddr_), addrSize);
if (-1 != rc)
{
LOG_INFO("Server socket successfully initialized");
rc = listen(this->getSrvrHandle(), 1);
if (-1 != rc)
{
LOG_INFO("Socket in listen mode");
rtn = true;
}
else
{
LOG_ERROR("Failed to set socket to listen");
rtn = false;
}
}
else
{
LOG_ERROR("Failed to bind socket, rc: %d", rc);
close(this->getSrvrHandle());
rtn = false;
}
}
else
{
LOG_ERROR("Failed to create socket, rc: %d", rc);
rtn = false;
}
return rtn;
}
上一步创建了一个服务器,并且保存了对其进行操作的描述符。这一步我们需要知道是否有客户端在连接,如果有则accept(第一步,判断是否有链接,第二步再accept,这样就不会阻塞了):
// ready: 表示是否有客户端连接(还未accept)
// error: 表示服务器是否有错误
// timeout: 0 表示马上返回,否则等待指定的时间(毫秒),直到超时返回。
bool TcpServer::serverPoll(int timeout, bool & ready, bool & error)
{
timeval time;
fd_set read, write, except;
int rc = -1;
bool rtn = false;
ready = false;
error = false;
// The select function uses the timeval data structure
time.tv_sec = timeout / 1000;
time.tv_usec = (timeout % 1000) * 1000;
FD_ZERO(&read);
FD_ZERO(&write);
FD_ZERO(&except);
if(-1 == this->getSrvrHandle())
return false;
FD_SET(this->getSrvrHandle(), &read); // 将服务器的描述符加入到可读的子集中.如果可读,则表示有客户端连接,然后我们就可以调用accept连接它了.
FD_SET(this->getSrvrHandle(), &except); // 将服务器的描述符加入到是否有异常的子集中.
// If both fields of the timeval structure are zero, then select() returns immediately. (This is useful for polling.) If timeout is NULL (no timeout), select() can block indefinitely(释放cpu的block).
rc = select(this->getSrvrHandle()+1, &read, &write, &except, &time); // +1 是必须的。见下文关于select调用的分析。
if (-1 != rc)
{
// 成功调用 select 之后我们判断 server 的描述符是否在可读的子集中,如果存在,则表示有客户端的连接进来了。
if (FD_ISSET(this->getSrvrHandle(), &read)) {
ready = true;
rtn = true;
}
else if(FD_ISSET(this->getSrvrHandle(), &except)) {
error = true;
rtn = true;
}
else {
LOG_WARN("Select returned, but no flags are set");
rtn = true;
}
}
else
{
LOG_ERROR("Socket select function failed", rc, errno);
rtn = false;
}
return rtn;
}
在上一步判断有客户端连接之后,我们再 accept,这样可以实现非阻塞的 accept 的调用了:
bool TcpServer::makeConnect()
{
bool rtn = false;
int rc = -1;
int disableNodeDelay = 1;
int err = 0;
if (!this->isConnected()) // 初始化为 false
{
this->setConnected(false);
if (-1 != this->getSockHandle())
{
close(this->getSockHandle());
}
// accept 调用会返回一个描述符,表示这个服务器与客户端的连接,对这个描述符 send & recv 可以向该连接的客户端发送和接收数据。
rc = accept(this->getSrvrHandle(), NULL, NULL);
if (-1 != rc)
{
this->setSockHandle(rc); // 保存这个描述符,因为每一次 accept 调用就会创建一个新的连接,而该描述符表示这个连接。如果有多个客户端连接,则创建一个队列,保存所有accept返回的描述符。
LOG_INFO("Client socket accepted");
// The set no delay disables the NAGEL algorithm
rc = setsockopt(this->getSockHandle(), IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val))disableNodeDelay);
err = errno;
if (-1 == rc)
{
LOG_WARN("Failed to set no socket delay, errno: %d, sending data can be delayed by up to 250ms", err);
}
this->setConnected(true); // 设置标志位
rtn = true;
}
else
{
LOG_ERROR("Failed to accept for client connection");
rtn = false;
}
}
else
{
LOG_WARN("Tried to connect when socket already in connected state");
}
return rtn;
}
服务器这边就是先调用 creatServer
,然后再循环的调用 serverPoll
,判断 ready 是否为 true,如果 serverPoll 返回的 ready 为 true 表示有客户端在进行连接,则调用 makeConnect
连接该客户端。
如果服务器需要判断是否有新的消息到达并等待读取,也可以通过 select
调用来判断。只不过这次是 select
的是 连接 的描述符(就是 accept 调用返回的描述符),select( this->getSockHandle()+1,...)
,而不是 server 的描述符。
客户端就简单了,创建socke,然后 connect 就行了:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
exit(0);
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
perror("recv error");
exit(1);
}
buf[rec_len] = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);
}
需要注意的是,select 调用有poll
和block
两种工作方式,当timeout
参数的内容设置为 0
时是poll
操作(select调用会立即返回,这种情况下需要轮询调用,如果没有适当的线程同步操作,会导致cpu占用高);而当timeout
参数本身为 NULL
时是block操作,此时select调用在没有可读或者可写的socket的情况下会阻塞调用,导致线程睡眠,直到条件满足才会被唤醒。
适当的利用block
模式下select
调用的返回条件(select在有socket可读时返回),可以精确控制线程的运行和睡眠。这种情况适用于,当只有 一个 work thread,并且有许多工作需要这个thread做(例如,除了处理读socket操作外,还要处理写socket操作和其他任务)。
例如: 在 tacopie
网络库中,使用名为 “self-pipe”的 trick ,当其他任务需要被处理时,就触发一下self-pipe,使得select调用返回,线程能够运行其他任务。而且,为了加快响应,self-pipe的socket类型设置为UDP类型, see link and link.
另外还需要注意的一个问题,见链接 howto prevent a process from terminating when writing to a broken pipe
ref link:
- http://beej-zhtw.netdpi.net/07-advanced-technology/7-2-select
- http://blog.csdn.net/hguisu/article/details/7445768/
值得注意的是,当需要监控大量的socket时 select 调用不是很高效(因此,select调用一般适用于client端,server端考虑epoll调用):
On the userspace side, generating and reading the bit arrays can be made to take time proportional to the number of
fds
that you provided forselect()
. But on the kernel side, reading the bit arrays takes time proportional to the largestfd
in the bit array, which tends to be around the total number of fds in use in the whole program, regardless of how many fds are added to the sets inselect()
.
也就是说,无论你需要监控的描述符有多少个,在内核中select
调用永远监控最大描述符范围内的所有描述符,这就有点浪费。
Comparing the performance for 100,000 monitoring operations:
operations | poll | select | epoll
10 | 0.61 | 0.73 | 0.41
100 | 2.9 | 3.0 | 0.42
1000 | 35 | 35 | 0.53
10000 | 990 | 930 | 0.66
So using epoll
really is a lot faster once you have more than 10 or so file descriptors to monitor.
当需要监控大量的socket时(10个以上),使用以下调用替换select:
- Linux/Unix: epoll()
- BSDs (including Darwin): kqueue()
- Solaris: evports and /dev/poll
如果需要考虑统一的接口,可以使用 libevent 库。
- https://jvns.ca/blog/2017/06/03/async-io-on-linux–select–poll–and-epoll/
- https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
- http://www.wangafu.net/~nickm/libevent-book/01_intro.html
- http://blog.csdn.net/tennysonsky/article/details/45745887