首页 > 代码库 > 《网络编程》套接字编程简介

《网络编程》套接字编程简介

       本节介绍的套接字是可以实现不同计算机之间的远程进程间通信。套接口是网络进程的 ID,在网络中每一个节点都有一个网络地址,也就是 IP 地址,两个进程间通信时,首先要确定各自所在网络节点的网络地址。但是,网络地址只要确定进程所在的计算机,由于一台计算机上同时可能有多个网络进程,所以仅凭网络地址还不能确定是网络中的哪一个进程,因此套接口中还需要其他信息,也就是端口。在一台计算机中,一个端口号只能分配给一个进程,所以,进程和端口之间是一一对应的关系。因此,使用端口号和网络地址的组合就能唯一地确定整个网络中的一个网络进程。

套接字地址结构

        把网络应用程序中所用到的网络地址和端口号信息放在一个结构体中,也就是套接口地址结构。大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义它自己的套接口地址结构,套接口地址结构都是以 sockaddr_ 开头,并以每个协议中名中的两个字母作为结尾。

/* 套接字编程简介 */

/* 通用套接字地址结构 */
/* 定义于 <sys/socket.h> 头文件中 */

struct sockaddr{

    uint8_t     sa_len;
    sa_family_t sa_family;  /* address family: AF_xxx value */
    char        sa_data[14];/* protocol-specific address */
};
/*
 * 说明:
 * 当套接字地址作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是以指向该结构的指针)来传递;
 * 因此,必须把特定协议域的套接字地址结构强制转换为通用套接字地址结构类型;
 */

/* IPv4 套接字地址结构 */
/* 定义于 <netinet/in.h> 头文件中 */

struct in_addr{

    in_addr_t   s_addr; /* 32-bit IPv4 address */
                        /* network byte ordered */
};

struct sockaddr_in{

    uint8_t         sin_len;    /* length of struct (16) */
    sa_family_t     sin_family; /* AF_INET */
    in_port_t       sin_port;   /* 16-bit TCP or UDP port number */
                                /* network byte ordered */
    struct in_addr  sin_addr;   /* 32-bit IPv4 address */
                                /* network byte ordered */
    char            sin_zero[8];/* unused */
};
/* 说明:
 * sin_len 是简化了长度可变套接字地址结构的处理,POSIX 规范并不要求有此成员;
 * sin_bzero 使所以套接字地址结构大小至少为 16 字节;
 * s_addr、sin_family、sin_port 是 POSIX 数据类型。in_addr_t 数据类型必须是一个至少 32 位的无符号整型,in_port_t 必须是一个至少 16 位的无符号整型,
 * 而 sa_family_t 通常是一个 8 位无符号整数,但在不支持长度字段的实现中,它则是 16 位无符号整数;
 * IPv4 地址和 TCP 或 UDP 端口号在套接字地址结构中是以 网络字节序来 存储的;
 */

/* IPv6 套接字地址结构 */
/* 定义于 <netinet/in.h> 头文件中 */

struct in6_addr{

    uint8_t     s6_addr[16];    /* 128-bit IPv6 address */
                                /* network byte ordered */
};

#define SIN6_LEN                    /* required for compile-time tests */
struct sockaddr_in6{

    uint8_t         sin6_len;       /* length of this struct (28) */
    sa_family_t     sin6_family;    /* AF_INET6 */
    in_port_t       sin6_port;      /* transport layer port# */
                                    /* network byte ordered */
    uint32_t        sin6_flowinfo;  /* flow information, undefined */
    struct in6_addr sin6_addr;      /* IPv6 address */
                                    /* network byte ordered */
    uint32_t        sin6_scope_id;  /* set of interfaces for a scope */
};
/* 说明:
 * 若系统支持套接字地址结构中的长度字段,则 SIN6_LEN 常值必须定义;
 * sin6_flowinfo 字段分成两个字段:低序 20 位是流标,高序 12 位保留;
 * 对于具备范围的地址,sin6_scope_id 字段标识其范围;
 */

/* 为了使用 IPv6 的使用,定义了新的通用套接字地址结构 */
/* 定义于 <netinet/in.h> 头文件中 */

struct sockaddr_storage{

    uint8_t         ss_len;     /* length of this struct (implementation dependent) */
    sa_family_t     ss_family;  /* address family: AF_xxx value */
    /* implementation-dependent elements to provide:
     * 1) alignment sufficient to fulfill the alignment requirments of all socket address types that the system supports. 
     * 2) enough storage to hold any type of socket address that the system suports.
     */
};
/* 说明:
 * 若系统支持任何套接字地址结构有对齐需要,则该新通用地址结构能够满足最苛刻的对齐要求;
 * 新地址结构能够容纳系统支持的任何套接字地址结构;
 */

字节排序

        计算机在内存中的数据存储有两种方式:一种是小端字节序,即内存低地址存储数据低字节,内存高地址存储数据高字节;另一种是大端字节序,即内存低地址存储数据高字节,内存高地址存储数据低字节;具体如下图所示:



        网络字节序采用的是大端字节序。某个系统所采用的字节序称为主机字节序(也称处理器字节序),主机字节序可能是小端字节序,也有可能是大端字节序。在网络协议中处理多字节数据时采用的都是网络字节序,即大端字节序,而不是主机字节序。要把主机字节序和网络字节序相对应,则必须采用字节序转换函数,以下是主机字节序和网络字节序之间的转换函数:

/*
 * 函数功能:主机字节序和网络字节序之间的转换;
 * 返回值:返回对应类型表示的字节序;
 * 函数原型:
 */
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);//返回值:以网络字节序表示的32位整型数;
uint16_t htons(uint16_t hostint16);//返回值:以网络字节序表示的16位整型数;

uint32_t ntohl(uint32_t netint32);//返回值:以主机字节序表示的32位整型数;
uint16_t ntohs(uint16_t netint16);//返回值:以主机字节序表示的16位整型数;
/*
 * 说明:
 * 从以上函数我们可以发现:
 * h表示“主机(host)”字节序,n表示“网络(network)”字节序;
 * l表示“长(long)”整型,s表示“短(short)”整型;
 *
 */

字节操作函数

/*
 * 函数功能:字节操作;
 * 函数原型:
 */
#include <strings.h>
void bzero(void *dest, size_t nbytes);//将dest所存储的数据前nbytes字节初始化为0;
void bcopy(const void *str, void *dest, size_t nbytes);//将str所存储的数据前nbytes字节复制到dest中;
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);//比较两个字符的前nbytes字节的大小;

void *memset(void *dest, int c, size_t len);//将dest所存储的数据前len字节初始化为c;
void *memcopy(void *dest,const void *src, size_t nbytes);//将src所存储的数据前nbytes字节复制到dest中;
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);//比较两个字符的前nbytes字节的大小;


地址转换函数

       若要把网络字节序的地址打印成我们能够理解的格式,则需要转换函数把网络字节序转换成我们可读的地址格式,inet_ntop 和 inet_pton 这两个函数把对 IP 地址格式进行转换,其定义如下:

#include<arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
//若成功则返回地址字符串指针,出错则返回NULL;

int inet_pton(int domain, const char *restrict str, void *restrict addr);
//若成功则返回1,格式无效则返回0,出错则返回-1;
/*
 * 说明:
 * inet_ntop 是将网络字节序的二进制地址转换成文本字符串格式;
 * inet_pton 是将文本字符串格式转换成网络字节序的二进制地址;
 * 参数domain只能取值:AF_INET 或 AF_INET6;
 */

 readn 、 writen 和 readline 函数

        字节流套接字上的 read 和 write 函数不同于普通的文件 I/O。当我们要求多次输入或输出时,必须多次调用 read 和 write 函数,这样比较麻烦。所以我们经常使用 readn 或 writen 函数。

#include "unp.h"
/* 函数功能:读/写 多个字节流;
* 返回值:读或写的字节数,若出错则返回-1;
* 函数原型:
*/
ssize_t readn(int filedes, void *buff, size_t nbytes);
ssize_t writen(int filedes, const void *buff, size_t nbytes);
ssize_t readline(int filedes, void *buff, size_t maxlen);

其具体实现如下:

/* include readn */
#include	"unp.h"

ssize_t						/* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
	size_t	nleft;
	ssize_t	nread;
	char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;		/* and call read() again */
			else
				return(-1);
		} else if (nread == 0)
			break;				/* EOF */

		nleft -= nread;
		ptr   += nread;
	}
	return(n - nleft);		/* return >= 0 */
}
/* end readn */

/* include writen */
#include	"unp.h"

ssize_t						/* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
	size_t		nleft;
	ssize_t		nwritten;
	const char	*ptr;

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

		nleft -= nwritten;
		ptr   += nwritten;
	}
	return(n);
}
/* end writen */

/* include readline */
#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);
}
/* end readline */

ssize_t
Readline(int fd, void *ptr, size_t maxlen)
{
	ssize_t		n;

	if ( (n = readline(fd, ptr, maxlen)) < 0)
		err_sys("readline error");
	return(n);
}

参考资料:

《Unix 网络编程》

《网络编程》套接字编程简介