16.1 套接字描述符
套接字是通信端点的抽象,用套接字描述符访问套接字。套接字描述符本质上是一个文件描述符,但是不是所有接受文件描述符的函数都接受套接字描述符。
为创建一个套接字,调用socket函数。
套接字通信是双向的,可以采用shutdown函数来禁止一个套接字的I/O。
套接字类型 | 描述 |
---|---|
SOCK_DGRAM | 固定长度、无连接、不可靠的报文传递 |
SOCK_RAW | IP协议的数据报接口 |
SOCK_SEQPACKET | 固定长度、有序、可靠、面向连接的报文传递 |
SOCK_STREAM | 有序、可靠,双向、面向连接的字节流 |
16.2 寻址
16.2.1 字节序
大端序,最高有效字节在低地址;小端序,最高有效字节在高地址。
主机序由处理器架构决定,intel的处理器通常是小端序。网络序是大端序。
例子
16进制数值 0x04030201 的最高有效字节是最左边 04,最低有效字节在右边 01
在大端序中,04在低地址,01在高地址;在小端序中则相反
16.2.2 地址格式
AF_INET的地址格式为sockaddr_in,而AF_NET6地址为sockaddr_in6,它们均被强制转换成通用地址格式sockaddr输入到套接字例程。
inet_ntop 将网络字节序的二进制地址转换成文本字符串格式。
inet_pton 将文本字符串格式转换成网络字节序的二进制地址。
有很多的查询函数能用于查询地址信息映射,包括网络名和网络编号,协议编号和协议名,服务名和端口号等。
例子
#include "apue.h"
#include <netdb.h>
#include <arpa/inet.h>
void print_family(struct addrinfo *aip) {
printf(" family ");
switch (aip->ai_family) {
case AF_INET:
printf("inet");
break;
case AF_INET6:
printf("inet6");
break;
case AF_UNIX:
printf("unix");
break;
case AF_UNSPEC:
printf("unspecified");
break;
default:
printf("unknown");
}
}
void print_type(struct addrinfo *aip) {
printf(" type ");
switch (aip->ai_socktype) {
case SOCK_STREAM:
printf("stream");
break;
case SOCK_DGRAM:
printf("datagram");
break;
case SOCK_SEQPACKET:
printf("seqpacket");
break;
case SOCK_RAW:
printf("raw");
break;
default:
printf("unkown type");
}
}
void print_protocol(struct addrinfo *aip) {
printf(" protocol ");
switch (aip->ai_protocol) {
case 0:
printf("default");
break;
case IPPROTO_TCP:
printf("TCP");
break;
case IPPROTO_UDP:
printf("UDP");
break;
case IPPROTO_RAW:
printf("raw");
break;
default:
printf("unknown protocol");
}
}
void print_flags(struct addrinfo *aip) {
printf("flags");
if (aip->ai_flags == 0) {
printf(" 0");
} else {
if (aip->ai_flags&AI_PASSIVE)
printf(" passive");
if (aip->ai_flags & AI_CANONNAME)
printf(" canon");
if (aip->ai_flags & AI_NUMERICSERV)
printf(" numsrv");
if (aip->ai_flags & AI_V4MAPPED)
printf(" v4mapped");
if (aip->ai_flags & AI_ALL)
printf(" all");
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
struct sockaddr_in *sinp;
const char *addr;
int err;
char abuf[INET_ADDRSTRLEN];
char service[1024], host[1024];
if (argc != 3)
err_quit("usage: %s nodename service", argv[0]);
hint.ai_flags = AI_CANONNAME;
hint.ai_family = 0;
hint.ai_socktype = 0;
hint.ai_protocol = 0;
hint.ai_addrlen = 0;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
//从服务名主机名获取socket地址,返回的是一个链表
if ((err = getaddrinfo(argv[1], argv[2], &hint, &ailist)) != 0)
err_quit("getaddrinfo error: %s", gai_strerror(err));
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
print_flags(aip);
print_family(aip);
print_type(aip);
print_protocol(aip);
printf("\n\thost %s", aip->ai_canonname?aip->ai_canonname:"-");
if (aip->ai_family == AF_INET) {
sinp = (struct sockaddr_in *)aip->ai_addr;
addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN);
printf(" address %s", addr?addr:"unknown");
printf(" port %d", ntohs(sinp->sin_port));
}
printf("\n");
//从socket地址获取主机名和服务名
if(getnameinfo((struct sockaddr *)aip->ai_addr, sizeof(struct sockaddr), host, 1024, service, 1024, 0) != 0)
err_sys("getnameinfo error");
else {
printf("getnameinfo, host: %s, service: %s\n", host, service);
}
}
exit(0);
}
16.2.3 套接字与地址关联
对于服务器,需要将接收客户端请求的服务器套接字关联一个众所周知的地址。
使用bind函数来关联地址和套接字。调用getsockname函数来发现绑定到套接字上的地址。调用getpeername函数来找到对
方的地址。
指定的地址必须有效;地址中的端口号必须不小于1024。
16.3 建立连接
使用connect函数来建立连接。
如果用SOCK_DGRAM套接字调用connect,传送的报文的目标地址会设置成connect调用中所指定的地址,并且仅能接收来自指定地址的报文。
服务器调用listen函数来宣告它愿意接受连接请求。使用accept函数获得连接请求并建立连接。
16.3.1 有连接的例子
服务器端,运行后成为后台进程。在/etc/services中添加ruptime服务信息,指定一个端口,否则客户端找不到服务。
#include "apue.h"
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#define BUFLEN 128
#define QLEN 10
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
extern int initserver(int, const struct sockaddr *, socklen_t, int);
void
serve(int sockfd)
{
int clfd;
FILE *fp;
char buf[BUFLEN];
set_cloexec(sockfd); //fork子进程并执行exec之后,删除无用的fd
for (;;) {
if ((clfd = accept(sockfd, NULL, NULL)) < 0) { //接受请求,返回客户端fd
syslog(LOG_ERR, "ruptimed: accept error: %s",
strerror(errno));
exit(1);
}
set_cloexec(clfd);
if ((fp = popen("/usr/bin/uptime", "r")) == NULL) { //创建到uptime的管道
sprintf(buf, "error: %s\n", strerror(errno));
send(clfd, buf, strlen(buf), 0);
} else {
while (fgets(buf, BUFLEN, fp) != NULL) //从uptime管道读取数据
send(clfd, buf, strlen(buf), 0); //发送到客户端
pclose(fp); //关闭管道
}
close(clfd); //关闭客户端fd
}
}
int
main(int argc, char *argv[])
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err, n;
char *host;
if (argc != 1)
err_quit("usage: ruptimed");
if ((n = sysconf(_SC_HOST_NAME_MAX)) < 0)
n = HOST_NAME_MAX; /* best guess */
if ((host = malloc(n)) == NULL)
err_sys("malloc error");
if (gethostname(host, n) < 0)
err_sys("gethostname error");
daemonize("ruptimed"); //服务进程设置成守护进程
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME; //返回标准名
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) { //查询本机服务信息,这里需要提前添加ruptime到/etc/services
syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s",
gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
aip->ai_addrlen, QLEN)) >= 0) { //初始化服务器监听socket
serve(sockfd); //接受请求并响应
exit(0);
}
}
exit(1);
}
客户端程序
#include "apue.h"
#include <netdb.h>
#include <errno.h>
#include <sys/socket.h>
#define BUFLEN 128
extern int connect_retry(int, int, int, const struct sockaddr *,
socklen_t);
void
print_uptime(int sockfd)
{
int n;
char buf[BUFLEN];
while ((n = recv(sockfd, buf, BUFLEN, 0)) > 0) //从fd中读取数据,输出到标准输出
write(STDOUT_FILENO, buf, n);
if (n < 0)
err_sys("recv error");
}
int
main(int argc, char *argv[])
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err;
if (argc != 2)
err_quit("usage: ruptime hostname");
memset(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) //需要知道服务器host,获取地址列表
err_quit("getaddrinfo error: %s", gai_strerror(err));
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = connect_retry(aip->ai_family, SOCK_STREAM, 0,
aip->ai_addr, aip->ai_addrlen)) < 0) { //创建连接,返回客户端fd
err = errno;
} else {
print_uptime(sockfd);
exit(0);
}
}
err_exit(err, "can't connect to %s", argv[1]);
}
选择套接字类型
- 无连接套接字是无次序的
- 数据包的最大尺寸不同
- 无连接套接字可能会丢失
16.3.2 无连接例子
服务器端,在启动之前,要修改/etc/services,添加ruptime的udp服务
#include "apue.h"
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#define BUFLEN 128
#define MAXADDRLEN 256
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
extern int initserver(int, const struct sockaddr *, socklen_t, int);
void
serve(int sockfd)
{
int n;
socklen_t alen;
FILE *fp;
char buf[BUFLEN];
char abuf[MAXADDRLEN]; //数组内存已经分配好了
struct sockaddr *addr = (struct sockaddr *)abuf; //转换为sockaddr类型
set_cloexec(sockfd);
for(;;) {
alen = MAXADDRLEN;
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, addr, &alen)) <0) { //从监听套接字读取数据,addr保存客户端地址
syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno));
exit(1);
}
if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
sprintf(buf, "error: %s\n", strerror(errno));
sendto(sockfd, buf, strlen(buf), 0, addr, alen); //向客户端地址发送数据,sockfd指服务端套接字
} else {
if (fgets(buf, BUFLEN, fp) != NULL)
sendto(sockfd, buf, strlen(buf), 0, addr, alen);
pclose(fp);
}
}
}
int
main(int argc, char *argv[])
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err, n;
char *host;
if (argc != 1)
err_quit("usage: ruptimed");
if ((n = sysconf(_SC_HOST_NAME_MAX)) < 0)
n = HOST_NAME_MAX; /* best guess */
if ((host = malloc(n)) == NULL)
err_sys("malloc error");
if (gethostname(host, n) < 0)
err_sys("gethostname error");
daemonize("ruptimed"); //服务进程设置成守护进程
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME; //返回标准名
hint.ai_socktype = SOCK_DGRAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) { //查询本机服务信息,这里需要提前添加ruptime到/etc/services
syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s",
gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = initserver(SOCK_DGRAM, aip->ai_addr,
aip->ai_addrlen, 0)) >= 0) { //初始化服务器监听socket
serve(sockfd); //接受请求并响应
exit(0);
}
}
exit(1);
}
客户端
#include "apue.h"
#include <sys/socket.h>
#include <netdb.h>
#include <errno.h>
#define BUFLEN 128
#define TIMEOUT 20
void sigalrm(int signo) {
}
void print_uptime(int sockfd, struct addrinfo *aip) {
int n;
char buf[BUFLEN];
buf[0] = 0;
if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0) //向服务器地址发送数据,服务器向sockfd返回数据
err_sys("sendto error");
alarm(TIMEOUT); //设置定时器
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) { //等待从sockfd返回数据,直到超时
if (errno != EINTR)
alarm(0);
err_sys("recv error");
}
alarm(0);
write(STDOUT_FILENO, buf, n); //写入标准输出
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err;
struct sigaction sa;
if (argc != 2)
err_quit("usage: ruptime hostname");
sa.sa_handler = sigalrm;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask); //清空设置
if (sigaction(SIGALRM, &sa, NULL) < 0) //添加对警告时钟的处理函数
err_sys("sigaction error");
memset(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_DGRAM; //数据报
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) //获取提供数据报服务的地址
err_quit("getaddrinfo error: %s", gai_strerror(err));
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) { //创建socket
err = errno;
} else {
print_uptime(sockfd, aip);
exit(0);
}
}
fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err));
exit(1);
}
16.4 套接字选项
套接字可以设置和查询选项,来控制套接字的特性。
例子
#include <sys/socket.h>
#include "apue.h"
#include <errno.h>
int initserver(int type, struct sockaddr *addr, socklen_t alen, int qlen) {
int fd, err;
int reuse = 1;
if ((fd = socket(addr->sa_family, type, 0)) < 0) //创建套接字
return -1;
//SOL_SOCKET 通用套接字选项
//IPPROTO_TCP TCP选项
//IPPROTO_IP IP选项
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)))
if (bind(fd, addr, alen) < 0) { //把套接字和传入的地址绑定,传入的地址对应对外提供的服务
printf("bind error");
goto errout;
}
if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
if (listen(fd, qlen) < 0) { //表示套接字可以进行监听请求,qlen指定监听队列长度
printf("listen error");
goto errout;
}
}
return fd; //返回创建好的套接字
errout:
err = errno;
close(fd);
errno = err;
return -1;
}
16.5 带外数据
带外数据是一些通信协议支持的可选功能,比普通数据以更高优先级传输。带外数据先行传输,即使传输队列已经有数据。
TCP支持带外数据,但是UDP不支持。TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。
16.6 非阻塞和异步IO
recv 函数没有数据可用时会阻塞等待。当套接字输出队列没有足够空间来发送消息时,send 函数会阻塞。
在套接字非阻塞模式下,行为会改变。
在基于套接字的异步I/O中,当从套接字中读取数据时,或者当套接字写队列中空间变得可用时,可以安排要发送的信号SIGIO。