Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Linux网络编程

18
Jun
2009

Linux网络编程

By Alex
/ in C,Linux
/ tags Linux编程, 网络编程
0 Comments
系统编程接口
错误码
代码 名字 说明
0   Success
1 EPERM Operation not permitted
2 ENOENT No such file or directory
3 ESRCH No such process
4 EINTR Interrupted system call
5 EIO Input/output error
6 ENXIO No such device or address
7 E2BIG Argument list too long
8 ENOEXEC Exec format error
9 EBADF Bad file descriptor
10 ECHILD No child processes
11 EAGAIN

Try again  / Resource temporarily unavailable

提示操作将被阻塞,但是请求了非阻塞操作

对于send(),可能原因包括:

  1. 使用 fcntl()显式的将文件描述符标记为非阻塞的
  2. 传递 MSG_DONTWAIT标记给 send()
  3. 使用套接字选项 SO_SNDTIMEO设置了发送超时
  4. 发送的消息无法放入(满了)套接字缓冲时,send()默认会阻塞。但是如果套接字被设置为non-blocking I/O模式,则会返回EAGAIN
12 ENOMEM Cannot allocate memory
13 EACCES Permission denied
14 EFAULT Bad address
15 ENOTBLK Block device required
16 EBUSY Device or resource busy
17 EEXIST File exists
18 EXDEV Invalid cross-device link
19 ENODEV No such device
20 ENOTDIR Not a directory
21 EISDIR Is a directory
22 EINVAL Invalid argument
23 ENFILE Too many open files in system
24 EMFILE Too many open files
25 ENOTTY Inappropriate ioctl for device
26 ETXTBSY Text file busy
27 EFBIG File too large
28 ENOSPC No space left on device
29 ESPIPE Illegal seek
30 EROFS Read-only file system
31 EMLINK Too many links
32 EPIPE Broken pipe
33 EDOM Numerical argument out of domain
34 ERANGE Numerical result out of range
35   Resource deadlock avoided
36   File name too long
37   No locks available
38   Function not implemented
39   Directory not empty
40   Too many levels of symbolic links
41   Unknown error 41
42   No message of desired type
43   Identifier removed
44   Channel number out of range
45   Level 2 not synchronized
46   Level 3 halted
47   Level 3 reset
48   Link number out of range
49   Protocol driver not attached
50   No CSI structure available
51   Level 2 halted
52   Invalid exchange
53   Invalid request descriptor
54   Exchange full
55   No anode
56   Invalid request code
57   Invalid slot
58   Unknown error 58
59   Bad font file format
60   Device not a stream
61   No data available
62   Timer expired
63   Out of streams resources
64   Machine is not on the network
65   Package not installed
66   Object is remote
67   Link has been severed
68   Advertise error
69   Srmount error
70   Communication error on send
71   Protocol error
72   Multihop attempted
73   RFS specific error
74   Bad message
75   Value too large for defined data type
76   Name not unique on network
77   File descriptor in bad state
78   Remote address changed
79   Can not access a needed shared library
80   Accessing a corrupted shared library
81   .lib section in a.out corrupted
82   Attempting to link in too many shared libraries
83   Cannot exec a shared library directly
84   Invalid or incomplete multibyte or wide character
85   Interrupted system call should be restarted
86   Streams pipe error
87   Too many users
88   Socket operation on non-socket
89   Destination address required
90   Message too long
91   Protocol wrong type for socket
92   Protocol not available
93   Protocol not supported
94   Socket type not supported
95   Operation not supported
96   Protocol family not supported
97   Address family not supported by protocol
98   Address already in use
99   Cannot assign requested address
100   Network is down
101   Network is unreachable
102   Network dropped connection on reset
103   Software caused connection abort
104   Connection reset by peer
105   No buffer space available
106   Transport endpoint is already connected
107   Transport endpoint is not connected
108   Cannot send after transport endpoint shutdown
109   Too many references: cannot splice
110   Connection timed out
111   Connection refused
112   Host is down
113   No route to host
114   Operation already in progress
115   Operation now in progress
116   Stale NFS file handle
117   Structure needs cleaning
118   Not a XENIX named type file
119   No XENIX semaphores available
120   Is a named type file
121   Remote I/O error
122   Disk quota exceeded
123   No medium found
124   Wrong medium type
套接字选项
选项 说明
SO_REUSEADDR

当绑定源地址的时候,可以通过绑定到0.0.0.0:port来绑定到所有本地网络地址的对应端口上,也可以绑定到10.0.0.1:port来绑定到特定本地网络地址的端口

在默认设置下,没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:80以后,Socket B若是想要绑定10.0.0.0.1:80,那就会报 EADDRINUSE。因为Socket A已经绑定了所有ip地址的80端口,包括10.0.0.1:8000

如果Socket B设置了选项SO_REUSEADDR,那么:

  1. 它不可以绑定0.0.0.0:80
  2. 它可以绑定10.0.0.0.1:80, 也就是说,除非有Socket已经绑定到和它字面上一模一样的地址,才报错

此外SO_REUSEADDR还可以允许绑定TIME_WAIT状态的连接占据的源地址

SO_REUSEPORT 允许多个Socket绑定到完全相同的IP和端口
套接字简介

Berkeley sockets(BSD sockets,来自BSD 4.2,1983),是一套用于操控因特网套接字、UNIX域套接字的接口标准,是进程间通信的一种方式。

POSIX sockets与Berkeley sockets的差别很小。大部分的现代操作系统实现了Berkeley sockets接口,甚至包括Microsoft的Winsock。

五元组

一个套接字(连接)由五元组来标识: <protocol>, <src addr>, <src port>, <dest addr>, <dest port>

其中:

  1. protocol在创建 socket()时设置
  2. src addr / src port 在 bind()时设置
  3. dest addr / dest port在 connect()时设置

虽然UDP不需要connect(),但是dest addr / dest port会在第一次发送数据数据时由系统隐式设置。

套接字API

这套API主要包含了以下头文件:

头文件 说明 
sys/socket.h 包括BSD套接字的核心函数、数据结构
netinet/in.h 与AF_INET、AF_INET6地址族相关的内容 
sys/un.h 与PF_UNIX/PF_LOCAL地址族相关的内容
arpa/inet.h 包含一些操控IP地址的函数
netdb.h 用来转换协议名称、主机名称为数字化的地址
unistd.h UNIX标准头,包含很多系统调用(fork、pipe)的封装和I/O原语,例如read、write、close

包含的主要函数有:

 函数 说明 
socket() 创建以整数(描述符)来识别的、指定类型的套接字,并为其分配系统资源
C
1
2
3
4
5
6
7
8
9
10
11
/**
* 创建一个通信端点(endpoint)并返回套接字的文件描述符,如果出错返回-1
* domain
*   指定协议族:AF_INET、AF_INET6、AF_UNIX
* type
*   套接字类型:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW
* protocol
*   传输协议:IPPROTO_TCP、IPPROTO_SCTP、IPPROTO_UDP、IPPROTO_DCCP
*   这些协议定义在netinet/in.h,如果指定0,则根据domain、type推导默认值
*/
int socket(int domain, int type, int protocol);
bind() 一般用于服务器端,将一个socket与socket address结构体(包含本地IP、端口信息)关联
C
1
2
3
4
5
6
7
8
9
10
/**
* 为套接字分配地址,成功返回0否则返回-1
* sockfd
*   被分配地址的套接字的描述符
* my_addr
*   代表地址的sockaddr结构的指针
* addrlen
*   指定sockaddr结构的长度
*/
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
listen() 用于服务器端,导致绑定的TCP socket进入监听(LISTENING)状态
C
1
2
3
4
5
6
7
8
9
/**
* 仅用于面向流(面向连接)的套接字服务端
* sockfd
*   监听的套接字描述符
* backlog
*   排队入站请求数量,一旦请求被接受即移出队列
*   超过队列限制的请求被直接拒绝
*/
int listen(int sockfd, int backlog);
connect() 用于客户端,将本地空闲端口分配给套接字,对于TCP,该函数将尝试建立TCP连接
C
1
2
3
4
5
6
7
8
9
10
11
/**
* 该系统调用通过自己指定的套接字,连接到服务器端套接字
* 对于无连接的套接字类型,该调用不建立连接,只是用于说明数据报的默认目标
* sockfd
*   本地套接字描述符
* serv_addr
*   远程套接字监听地址
* addrlen
*   远程套接字地址的长度  
*/
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
accept() 用于服务器端,接受一个入站连接请求,创建并返回关联了socket pair的socket
C
1
2
3
4
5
6
7
8
9
10
/**
* 为连接请求创建一个新的套接字,并移出队列
* sockfd
*   监听套接字的描述符
* cliaddr
*   用于接收客户端地址信息的结构
* addrlen
*   sockaddr类型的长度
*/
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
send() TCP Socket的send()是一个异步调用,当数据送入socket send buffer以后就会返回。也就是说,在send()返回以后,数据仍然需要经历TCP拥塞控制等过程,才能被成功发送
recv() read返回值:0表示对端关闭了连接;-1表示发生错误
write()  
read()  
sendto() 用于UDP场景下的发送数据
recvfrom() 用于UDP场景下的接收数据
close()

关闭套接字的输入/输出通道,导致系统释放与套接字相关的资源,对于TCP,连接被关闭。对于客户端,即使connect()失败也要关闭套接字

该调用仅仅销毁了套接字的接口,套接字本身由OS内核负责销毁,某些情况下,套接字可能进入TIME_WAIT状态

shutdown()

类似于close(),但是可以实现“半关闭”,即只关闭输出通道或者输入通道

gethostbyname() 用于解析IPv4的主机名、IP地址
C
1
2
3
4
5
6
7
8
/**
* 通过DNS系统或者/etc/hosts来查找主机名对应的Internet地址信息
* 如果调用成功返回代表因特网地址的结构hostent,否则返回NULL指针
* 出错时通过h_errno可以检查具体原因
* name
*   主机名,例如gmem.cc
*/
struct hostent *gethostbyname(const char *name);
gethostbyaddr()
C
1
2
3
4
5
6
7
8
9
10
/**
* 类似于上面的函数,通过地址来查询
* addr
*   in_addr类型的指针,代表了主机的地址
* len
*   上面地址的长度
* type
*   地址族类型,例如AF_INET
*/
struct hostent *gethostbyaddr(const void *addr, int len, int type);
select()

轮询,等待所提供列表中一个或者多个套接字可读、可写或者有错误发生。具有以下缺点:

  1. 每次调用时,列表中所有套接字的文件描述符需要拷贝到内核空间
  2. 每次调用时,需要在内核态遍历文件描述符
  3. 支持的文件描述符数量较小,默认1024
poll() 与select()类似,但是描述文件描述符的方式不同,select()使用fd_set结构,而poll()使用pollfd结构
epoll() Linux内核为处理大批量文件描述符而作了改进的poll
getsockopt() 获取某个套接字选项当前的值
setsockopt() 设置某个套接字选项的值
Linux代码示例
Echo示例 
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
 
#define DEFAULT_PORT 1918
 
const char* ip2txt( struct sockaddr_in addr, char* buf );
 
int main( void )
{
    int sock_desc, client_sock, read_size;
    struct sockaddr_in server, client;
    socklen_t socklen = sizeof ( server );
    char client_message[2000], ipv4_buf[INET_ADDRSTRLEN];
    struct timeval timeout;
    timeout.tv_sec = 3;
    timeout.tv_usec = 0;
 
    setvbuf( stdout, NULL, _IONBF, 0 );
 
    //创建套接字,返回一个描述符
    sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    if ( sock_desc == -1 )
    {
        puts( "Failed to create socket" );
        return 1;
    }
    //监听套接字结构
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( DEFAULT_PORT );
    //绑定本地监听地址到描述符
    if ( bind( sock_desc, ( struct sockaddr * ) &server, socklen ) < 0 )
    {
        puts( "Binding failed" );
        return 1;
    }
    else
    {
        printf( "Binding to local socket address: %s:%d\n", ip2txt( server, ipv4_buf ), DEFAULT_PORT );
    }
    //开始监听,等待队列容量10
    listen( sock_desc, 10 );
    for ( ;; )
    {
        //接收一个客户端连接,返回代表此套接字的描述符
        client_sock = accept( sock_desc, ( struct sockaddr * ) &client, &socklen );
        printf( "Connection from %s:%d accepted\n", ip2txt( client, ipv4_buf ), ntohs( client.sin_port ) );
        //设置读取超时,此方法用于Linux,Cygwin无效
        if ( setsockopt( client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval) ) == 0 )
        {
            printf( "recv() timeout set to %ds\n", ( int ) timeout.tv_sec );
        }
        //循环读取数据,该方法会立即返回
        while ( ( read_size = recv( client_sock, client_message, sizeof ( client_message ), 0 ) ) > 0 )
        {
            //把数据原样写给客户端
            printf( "Received message: %s\n", client_message );
            write( client_sock, client_message, strlen( client_message ) );
        }
 
        //如果超时后仍然读取不到数据、或者出现其他错误,会返回-1
        puts( "Error recv()." );
    }
    return 0;
}
 
const char* ip2txt( struct sockaddr_in addr, char* buf )
{
    //该函数用来将网络字节序的整数转换为点号分隔的IP地址格式
    return inet_ntop( AF_INET, &addr.sin_addr, buf, INET_ADDRSTRLEN );
}

客户端代码示例:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void client()
{
    int client_sock_desc;
    struct sockaddr_in server;
    char client_message[2000], server_message[2000];
    client_sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr( "192.168.0.90" );
    server.sin_port = htons( DEFAULT_PORT );
    socklen_t socklen = sizeof ( server );
    //连接到服务器端
    if ( connect( client_sock_desc, ( struct sockaddr* ) &server, socklen ) < 0 )
    {
        printf( "Connect failed.\n" );
        return;
    }
    while ( 1 )
    {
        printf( "Enter message: \n" );
        scanf( "%s", client_message );
        //发送消息
        if ( send( client_sock_desc, client_message, strlen( client_message ), 0 ) < 0 )
        {
            printf( "Send failed.\n" );
            return;
        }
        //接收回应
        if ( recv( client_sock_desc, server_message, sizeof ( server_message ), 0 ) < 0 )
        {
            printf( "Recv failed.\n" );
            return;
        }
        printf( "Message from server:%s\n", server_message );
    }
}
多进程服务器示例

一般情况下,对于网络服务器不适用“多进程”方式处理,因为网络服务器通常需要很多内存资源,fork()却会复制这些资源。这里只是做一个简单示例:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bind( server_sockfd, ( struct sockaddr * ) &server_address, server_len );
listen( server_sockfd, 5 );
signal( SIGCHLD, SIG_IGN ); //忽略子进程的退出
while ( 1 )
{
    client_len = sizeof ( client_address );
    //接受一个连接请求
    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &client_address, &client_len );
    //创建子进程处理请求
    if ( fork() == 0 )
    {
        //如果是子进程,读取消息并处理
        //注意子进程继承父进程打开的文件描述符
        read( client_sockfd, &ch, 1 );
        write( client_sockfd, &ch, 1 );
        close( client_sockfd );
        exit( 0 );
    }
    else
    {
        //如果是父进程,只需要关闭描述符
        close( client_sockfd );
    }
}
基于Select的异步I/O

select系统调用允许同时在多个底层文件描述符上等待输入的到达或输出的完成,避免在单个输入输出上的忙等待。下面是相关的函数说明:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <sys/time.h>
//下面这组宏用于对文件描述符集合进行操作
void FD_ZERO( fd_set *fdset );//将文件描述符集初始化为空集合
int FD_ISSET( int fd, fd_set *fdset ); //判断文件描述符fd是否存在于fdset中
void FD_CLR( int fd, fd_set *fdset ); //清除一个文件描述符
void FD_SET( int fd, fd_set *fdset ); //设置一个文件描述符
/**
* 测试文件描述符集中是否至少有一个文件描述符已经处于可读、可写、错误状态。
* 该函数在以下情况下返回:
* 1、readfds具有可读、writefds具有可写、errorfds存在描述符遇到错误条件
* 2、如果上述三个情况都没有发生,该调用在timeout后返回
* 当返回时,描述符集合被修改为指示哪些描述符可读、可写或者处于错误状态
*
* @param nfds 需要测试的文件描述符数量,从0到nfds-1
* 下面三个参数都可以被设置为空指针,表示不进行相应的测试
* @param readfds  读文件描述符
* @param writefds 写文件描述符
* @param errorfds 错误文件描述符
* @param timeout 超时时间,如果是空指针将一直阻塞
* @return 状态无变化的描述符总数,失败时返回-1并设置errno:
*         EBADF 无效文件描述符
*         EINTR 因中断而返回
*         EINVAL ndfs或者timeout取值错误
*/
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout );
struct timeval
{
    time_t tv_sec; /* 毫秒 */
    long tv_usec; /* 微秒 */
};

下面是一段示例代码,通过单进程服务多个客户端,并将请求值加1返回给客户端:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
 
#define DEFAULT_PORT 1918
 
int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;
    //创建客户端TCP套接字
    server_sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl( INADDR_ANY );
    server_address.sin_port = htons( DEFAULT_PORT );
    server_len = sizeof ( server_address );
    //绑定套接字描述符到本地地址
    bind( server_sockfd, ( struct sockaddr * ) &server_address, server_len );
    //在套接字描述符上监听
    listen( server_sockfd, 5 );
    FD_ZERO( &readfds );
    FD_SET( server_sockfd, &readfds ); //将监听描述符添加到集合中
    while ( 1 )
    {
        char ch;
        int fd;
        int nread;
        testfds = readfds;
        //一直等待,直到输入(连接请求)到达
        result = select( FD_SETSIZE, &testfds, ( fd_set * ) 0, ( fd_set * ) 0, ( struct timeval * ) 0 );
        for ( fd = 0; fd < FD_SETSIZE; fd++ )
        {
            //遍历所有就绪的可读描述符
            if ( FD_ISSET( fd, &testfds ) )
            {
                if ( fd == server_sockfd )
                {
                    //这是一个服务器监听套接字
                    //服务器端套接字就绪,说明有连接请求
                    client_len = sizeof ( client_address );
                    //生成客户套接字,并加入到读套接字集中
                    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &client_address, &client_len );
                    FD_SET( client_sockfd, &readfds );
                }
                else
                {
                    //这是一个客户套接字
                    ioctl( fd, FIONREAD, &nread ); //得到可读数据字节数
                    if ( nread == 0 )
                    {
                        close( fd );
                        FD_CLR( fd, &readfds ); //从读集合中移除该客户端
                    }
                    else
                    {
                        read( fd, &ch, 1 ); //读取一个字节
                        ch++;
                        write( fd, &ch, 1 ); //写回一个字节
                    }
                }
            }
        }
    }
}
UDP示例
C
1
2
3
4
5
6
7
8
9
sockfd = socket( AF_INET, SOCK_DGRAM, 0 );
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *( struct in_addr * ) *hostinfo->h_addr_list;
len = sizeof(address);
//向address发送一个数据报
sendto( sockfd, buffer, 1, 0, ( struct sockaddr * ) &address, len );
//从address接收数据报
recvfrom( sockfd, buffer, sizeof(buffer), 0, ( struct sockaddr * ) &address, &len );
TCP服务器示例
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include
#include
#include
#include
#include
#include <sys/types.h>
#include  
 
int main(int argc, char *argv[])
{
    int listenfd = 0, connfd = 0;
    struct sockaddr_in serv_addr;
 
    char sendBuff[1025];
    time_t ticks;
    // 下面的调用在内核中创建一个未命名套接字,返回一个整数 —— 套接字描述符
    // AF_INET 地址族,对于IPv4使用AF_INET
    // SOCK_STREAM 传输层协议类型,流式表示需要确认机制
    // 0 让内核决定默认协议,AF_INET + SOCK_STREAM -> TCP
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
 
    memset(&serv_addr, '0', sizeof(serv_addr));
    memset(sendBuff, '0', sizeof(sendBuff));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(5000);
    // 将serv_addr所指定的细节信息,绑定到listenfd。bind调用时可选的,如果不调用,内核可以随机选取监听端口
    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
 
    // 在此套接字上监听新连接请求,最多10个排队
    listen(listenfd, 10);
 
    while(1)
    {
        // accept导致当前线程休眠,当有新的客户端请求进入,并且完成3次握手后,线程醒来
        // 获得代表客户端套接字的套接字描述符
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
 
        ticks = time(NULL);
        snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&ticks));
        // 可以向套接字描述符中写数据
        write(connfd, sendBuff, strlen(sendBuff));
        // 关闭描述符
        close(connfd);
        sleep(1);
     }
}
TCP客户端示例
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include
#include
#include
#include
#include
#include
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    char recvBuff[1024];
    struct sockaddr_in serv_addr;
 
    if(argc != 2)
    {
        printf("\n Usage: %s  \n",argv[0]);
        return 1;
    }
 
    memset(recvBuff, '0',sizeof(recvBuff));
    // 创建一个套接字,客户端服务器没有区别
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    }
 
    memset(&serv_addr, '0', sizeof(serv_addr));
 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5000);
 
    if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    }
 
    // 客户端套接字一般都不需要绑定,由内核自由分配一个端口就足够了
 
    // 尝试连接到远程套接字(IP+端口)
    if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    }
    // 读取套接字,就像读取一个普通文件一样
    while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0)
    {
        recvBuff[n] = 0;
        if(fputs(recvBuff, stdout) == EOF)
        {
            printf("\n Error : Fputs error\n");
        }
    }
 
    if(n < 0)
    {
        printf("\n Read error \n");
    }
 
    return 0;
}
轮询机制对比
read

应用程序周期性的调用read来检查I/O状态,以完成数据的读取,性能最差。

select

维护一个最长1024的数组,保存文件描述符的状态,一次select可以遍历很多文件描述符。

C
1
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分 3 类,分别是 writefds、readfds 和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可)。当 select 函数返回后,通过遍历 fd_set,来找到就绪的描述符。
select 目前几乎在所有的平台上支持,其良好跨平台支持是它的一大优点。select 的一个缺点在于单个进程(Apache使用多进程方式解决此问题,尽管Linux进程很轻量,也是有代价的。而且进程间数据同步远比不上线程间同步的高效)能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

poll

类似于select,但是使用链表解决1024数组长度限制。

C
1
2
3
4
5
6
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select参数-值传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
从上面看,select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

效率最高,调用epoll后,如果没有发现可用的I/O事件,调用线程将会自动休眠,直到有事件发生后,内核将其唤醒。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 epoll 文件描述符,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议
int epoll_create(int size);
// 对指定描述符 fd 执行 op 操作控制,event 是与 fd 关联的监听事件
// op有三种:添加 EPOLL_CTL_ADD,删除 EPOLL_CTL_DEL,修改 EPOLL_CTL_MOD
//          分别添加、删除和修改对 fd 的监听事件
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 上的 IO 事件,最多返回 maxevents 个事件
int epoll_wait(int epfd, struct epoll_event * events,  int maxevents, int timeout);

在 select/poll 中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll 事先通过 epoll_ctl() 来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait 时便得到通知

epoll 的优点主要是一下几个方面:

  1. 监视的描述符数量不受限制,它所支持的 fd 上限是最大可以打开文件的数目,这个数字一般远大于 2048, 举个例子, 在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看, 一般来说这个数目和系统内存关系很大
  2. IO 的效率不会随着监视 fd 的数量的增长而下降。epoll 不同于 select 和 poll 轮询的方式,而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数
  3. 支持水平触发和边沿触发两种模式:
    1. 水平触发模式,文件描述符状态发生变化后,如果没有采取行动,它将后面反复通知,这种情况下编程相对简单,libevent 等开源库很多都是使用的这种模式
    2. 边沿触发模式,只告诉进程哪些文件描述符刚刚变为就绪状态,只说一遍,如果没有采取行动,那么它将不会再次告知。理论上边缘触发的性能要更高一些,但是代码实现相当复杂(Nginx 使用的边缘触发)
  4. mmap 加速内核与用户空间的信息传递。epoll 是通过内核与用户空间 mmap 同一块内存,避免了无谓的内存拷贝
← Maven知识集锦
备忘录模式 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Linux网络知识集锦
  • Linux进程间通信
  • Linux内核编程知识集锦
  • Linux IO编程
  • Linux编程知识集锦

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2