套接字编程简介

套接字地址结构

IPv4套接字地址结构

/* 定义在<netinet/in.h> 头文件中*/
struct in_addr {
    in_addr_t s_addr; 				/* 32位IPv4地址,网络字节序 */
};

struct sockaddr_in {
    unit8_t 		sin_len;		/* 结构体的长度 */
  	sa_family_t		sin_family;		/* AF_INET */
  	in_port_t		sin_port;		/* 16位的TCP或UDP端口号 */
  	struct in_addr	sin_addr;		/* 32位的IPv4地址,网络字节序 */
  	char			sin_zero[8];	/* 未使用 */
};
1
2
3
4
5
6
7
8
9
10
11
12

通用套接字地址结构

/* <sys/socket.h> */
struct sockaddr {
	unit8_t		sa_len;
	sa_family_t	sa_family;		/* AF_XXX */
	char		sa_data[4]		/* 特定协议的地址 */
};
1
2
3
4
5
6

套接字函数的某个参数就是指向某个通用套接字地址结构的一个指针,如bind函数的原型为:

int bind(int, struct sockaddr *, socklen_t);
1

这就要求对这些套接字函数的调用都必须将指向特定于协议的套接字地址结构的指针进行类型强制转换(casting),变成指向某个通用套接字地址结构的指针。

IPv6套接字地址结构

/* <netinet/in.h */
struct in6_addr {
	unit8_t s6_addr[16]; 		/* 128位IPv6地址 */
}
#define SIN6_LEN				/* 编译期测试使用 */
struct sockaddr_in6 {
	unit8_t			sin6_len;		/* 结构体的长度 */
	sa_family_t		sin6_family;	/* AF_INET6 */
	in_port_t		sin6_port;		/* 传输层端口号 */
	uint32_t		sin6_flowinfo;	/* 流信息,还未定义 */
	struct in6_addr	sin6_addr;		/* IPv6地址,网络字节序 */
	uint32_t		sin6_scope_id;	/* set of interfaces for a scope */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

新的通用套接字地址结构

新的通用套接字struct sockaddr_storage足以容纳系统所支持的任何套接字地址结构。

#include <netinet/in.h>

struct sockaddr_storage {
	unit8_t			ss_len; /* 结构体的长度 */
	sa_family_t		ss_family; /* 地址族: AF_XXX 值 */

	/*
	 * implementation-dependent elements to provide:
	 * a) alignment sufficient to fulfill the alignment requirements 
	 *    of all socket address types that the system supports;
	 * b) enough storage to hold any type of socket address that the
	 *    system supports.
	 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

字节操纵函数

字节排序函数

请看上一篇文章

inet_aton、inet_addr和inet_ntoa函数

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr); /* 返回: 若字符串有效返回1,否则0 */
in_addr_t inet_addr(const char *strptr); /* 返回:若字符串有效则返回32位的二进制网路字节序的IPv4地址,否则为`INADDR_NONE` */
char *inet_ntoa(struct in_addr inaddr);	/* 返回一个指向点分十进制数串的指针 */
1
2
3
4

这几个函数在点分十进制数串(例如192.168.0.1)与它的长度为32位的网络字节序二进制值间转换IPv4地址。

  • inet_aton将strptr所指向的C字符串转换成一个32位的网络字节序二进制值。若成功则返回1,否则返回0。
  • inet_addr进行相同的转换,返回值为32位的网络字节序二进制值。但是该函数出错时返回INADDR_NONE常值(通常是一个32位均为1的值)。这意味着点分十进制数串255.255.255.255(这是IPv4的有限广播地址)不能由该函数处理,因为它的二进制值被用来指示该函数失败。(如今inet_addr已被弃用,新的代码应该改用inet_aton函数
  • inet_ntoa函数将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串,由该函数的返回值所指向的字符串驻留在静态内存中,这意味着该函数是不可重入的。该函数以一个结构体而不是以指向该结构体的一个指针作为其参数。

inet_pton和inet_ntop函数

#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr); /* 返回:若成功返回1,出错返回-1 */
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); /* 返回:若成功返回指向结果的指针,若出错返回 NULL */
1
2
3
4

这两个函数的family参数既可以是AF_INET,也可以是AF_INET6。inet_pton将尝试转换strptr参数所指的字符串并通过addrptr指针存放二进制结果。inet_ntop进行相反的操作,从数值结果addrptr转换到表达式格式strptr。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。

为了有助于指定这个大小,<netinet/in.h>头文件中有如下定义:

#define INET_ADDRSTRLEN		16	/* IPv4 点分十进制 */
#define INET6_ADDRSTRLEN	46	/* IPv6 十六进制字符串 */
1
2

readn、writen和readline函数

read函数

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); /* 读取不到数据是返回 0,成功返回读取的字节数 */
1
2

从一个文件句柄读取数据。read函数会尝试从文件句柄fd中读取最多count字节的数据,并缓存到buf的起始位置。如果文件支持寻址,read操作会从文件的初始位置开始,每次文件的偏移量都会增加上一次所读取的字节数。如果文件的偏移量达到或者超过了文件的末尾,read将读取不到任何字节,并且返回0

读取成功时返回所读的字节数(0表示没有数据可读取),文件的位置也增加同样的字节数。出错时返回 -1,并且errno 也会设置相应的值。

write函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count); /* 成功返回写入的字节数,0表示没有写入数据。写入失败的时返回-1 */
1
2

write函数将buf中的数据写入fd所指向的文件中,并且最多count个字节。在某些情况下写入的数据可能少于count,比如物理存储空间不够、设置了RLIMIT_FSIZE资源限制、或者被信号处理函数中断了。对于一个支持寻址的文件,写入操作将在文件的偏移位置开始,文件的偏移量的增加和实际写入的字节数一致。如果文件的打开时方式是O_APPEND,文件的偏移位置是文件的末尾。文件的偏移量的修改和写入操作是原子的。

写入失败时会设置相应的errno值。

unp.h

字节流套接字上的read和write函数所表现的行为不同于通常的文件I/O。字节流套接字上调用read或write函数输入或输出的字节数可能会比请求的数量少,然而这不是bug,原因在于内核中用于套接字的缓冲区可能已经达到了极限,因此需要调用者再次调用read或write函数,以输入或输出剩余的字节。为了以防万一,可以封装这两个函数。

/* file: lib/unp.h */

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

/* Miscellaneous constants */
#define MAXLINE 4096 /* max text line length */
1
2
3
4
5
6
7
8
9

readn函数

/* file: lib/readn.c */
#include "unp.h"

/**
 * Read n bytes from a decriptor
 */
ssize_t readn(int fd, void *vptr, size_t n){
    size_t nleft; /* size_t: unsigned int */
    ssize_t nread; /* ssize_t: signed size_t */
    char *ptr;
    
    ptr = vptr;
    nleft = n;
    
    while(nleft > 0){
        if((nread = read(fd, ptr, nleft)) < 0) {
            /**
             * EINTR
             *  write: The call was interrupted by a signal before any data was written. 
             *  read: The call was interrupted by a signal before any data was read.
             *  sem_wait: The call was interrupted by a signal handler.
             *  recv: function was interrupted by a signal that was caught, before any data was available.
             */
            if(errno == EINTR)
                nread = 0;          /* The call was interrupted and call read() again */
            else
                return (-1);
        } else if(nread == 0)
            break;                  /* EOF */
        
        nleft -= nread;
        ptr += nread;
    }

    return (n - nleft);
}


// #include <fcntl.h>
// int main()
// {
//     char ptr[200];
//     int fh = open("./test.txt", O_RDONLY, 0777);
//     readn(fh, ptr, 34);
//     close(fh);
//     printf("%s", ptr);
// }
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

writen函数

/* file: lib/writen.c */
#include "unp.h"

ssize_t writen(int fd, const void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwriten;
    const char *ptr;

    ptr = vptr;
    nleft = n;
    
    while(nleft > 0){
        if((nwriten = write(fd, ptr, nleft) ) <= 0) {
            if(nwriten < 0 && errno == EINTR)
                nwriten = 0;        /* call write() again */
            else
                return -1;
        }

        nleft -= nwriten;
        ptr += nwriten;
    }

    return n;
}


// #include <fcntl.h>
// int main()
// {
//     int fh = open("./test.txt", O_RDWR | O_CREAT, 0777);
//     writen(fh, "those words should be writen in.\n", 34);
//     close(fh);
// }
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

readline 函数

/* file: lib/readline.c */
#include "unp.h"

static int	read_cnt;
static char	*read_ptr;
static char	read_buf[MAXLINE];

static ssize_t
my_read(int fd, char *ptr)
{

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return(-1);
		} else if (read_cnt == 0)
			return(0);
		read_ptr = read_buf;
	}

	read_cnt--;
	*ptr = *read_ptr++;
	return(1);
}

ssize_t
readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t	n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n'){
                break; /* newline is stored, like fgets() */
            }
				
		} else if (rc == 0) {
            *ptr = 0;
            return(n - 1);	/* EOF, n - 1 bytes were read */
		} else{
            return(-1);		/* error, errno set by read() */
        }
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}

/* 缓冲区中是否还有数据 */
ssize_t readlinebuf(void **vptrptr)
{
    if(read_cnt)
        *vptrptr = read_ptr;
    return read_cnt;
}


// #include <fcntl.h>

// int main(int argc, char const *argv[])
// {
//     int fd = open("./byteorder.c", O_RDONLY);
//     size_t maxlen = 100;
//     char vptr[maxlen];
    
//     ssize_t line = readline(fd, vptr, maxlen);

//     printf("%d", fd);
//     printf("%s", vptr);
//     return 0;
// }
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
75
76

注意

readline使用静态变量实现跨相继函数调用的状态信息维护,其结果是这些函数变得不可重入或者说非线性安全了。之后会有新的版本来代替。

套接字选项

getsockopt和setsockopt函数

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

/* 若成功,均返回0,出错返回-1 */
1
2
3
4
5
6

其中sockfd必须指向一个打开的套接字描述符,level指定系统中解释选项的代码或者为通用套接字代码,或者为某个特定于协议的代码(如SOL_SOCKET,IPROTO_IP等)。

optval是指向某个变量的指针,setsockopt从optval中取得optname选项的待设置新值,getsockopt则把optname选项的已有值保存到optval中。

套接字选项的使用示例可以查看redis的网络相关的源码——anet

fcntl函数

与代表“file control”的名字相符,fcntl函数提供了与网络编程相关的如下特性:

  • 非阻塞I/O。通过使用F_SETFL命令设置O_NONBLOCK文件状态标志,我们可以把一个套接字设置为非阻塞型。
  • 信号驱动式I/O。通过使用F_SETFL命令设置O_ASYNC文件状态标识,我们可以把一个套接字设置成一旦其状态发生变化,内核就产生一个SIGIO信号。
  • F_SETOWN命令允许我们指定用于接收SIGIOSIGURG信号的套接字属主(进程ID或进程组ID)。其中SIGIO信号是套接字被设置为信号驱动式I/O型后产生的,SIGURG信号是在新的带外数据到达套接字时产生的。F_GETOWN命令返回套接字的当前属主。
#include <fcntl.h>

int fcntl(int fd, int cmd, .../* int arg */);		/* 成功返回取决于cmd,出错返回 -1 */
1
2
3

fcntl函数的简单用法示例可参考redis源码 —— anet:套接字描述符设置阻塞-非阻塞

最近更新: 1/13/2019, 9:55:37 PM