1、一个简单的时间获取服务器的程序
#1.解压文件进入主目录 cd intro
#2.直接编译 gcc -o 1.out daytimetcpcli.c #错误提示如下: daytimetcpcli.c:1:17: fatal error: unp.h: No such file or directory #include "unp.h" ^ compilation terminated. #在../key目录下找到unp.h。继续提醒没有<sys/fiflo.h>从网上下载继续出问题。
#3.看到目录下有make文件,make一下:显示如下: peace@peace:~/workspace/unpv13e/unpv13e/intro$ make Makefile:1: ../Make.defines: No such file or directory make: *** No rule to make target '../Make.defines'. Stop.
从网下在下来源码然后编译就出了问题,百度,发现很多人跟我遇到同样的问题,还是参考下网友的经验,从README开始吧。
./configure # try to figure out all implementation differences
cd lib # build the basic library that all programs need
make # use "gmake" everywhere on BSD/OS systems
cd ../libfree # continue building the basic library
make
cd ../libroute # only if your system supports 4.4BSD style routing sockets
make # only if your system supports 4.4BSD style routing sockets
cd ../libxti # only if your system supports XTI
make # only if your system supports XTI
cd ../intro # build and test a basic client program
make daytimetcpcli
./daytimetcpcli 127.0.0.1
在编译../libfree时出现了下面的错误
错误提示inet_ntop.c中60行声明与原型申明/usr/include/arpa/inet.h不匹配。(#include一般所在文件都在/usr/include中)
经查验,最后一个参数,在inet.h中定义socklen_t,而inet_ntop.c中定义为size_t。在inet_ntop.c中加入如下代码即可:(插入到所有头文件之后)
#define size_t socklen_t
然后make通过。
接着编译
原来是daytime服务没有开启!
在后台运行./daytimetcpsvr,让服务器端后台执行,这里是借助srv程序来开启系统的daytime服务程序。
2、套接字篇
2.1 套接字简介
通常也被成为“网际套接字地址结构”,以sockaddr_in命名,定义在<netinet/in.h>头文件中。
struct in_addr
{
in_addr_t s_addr; // 32bits的ip地址,如0xFF000001 -> 127.0.0.1
};
struct sockaddr_in
{
uint8_t sin_len; /* length of structure (16) */
sa_family_t sin_family; /* AF_INET */
char sin_zero[8]; /* unused */
}; // sin_family,sin_port,sin_addr是一定支持的3个成员
IPv4地址和TCP或UDP端口号在套接字地址结构中总是以网络字节序(区别于主机字节序)来存储。
之所以网际地址(in_addr)是一个结构体,是因为以前这个结构体中允许访问2个16位的值,用于划分A、B、C类,而现在子网划分之后,这些union结构不再需要。
sin_zero字段未曾使用,但我们总是把该字段置为0,按照惯例,我们总是在填写前把整个结构置为0。
以上是IPv4套接字地址结构,然而套接字函数是通用的,并且总是接收一个套接字地址结构的指针(eg, sockaddr_in serv; bind(sockfd, (sockaddr *) &serv, sizeof(serv)); ),可以看到第二个参数被转成了sockaddr类型,这是通用套接字地址结构。在套接字函数定义的时候,还没有通用的指针类型void *,所以必须传入一个恰当的类型,否则会报错,于是在<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 */
};
IPv6地址为128位长,但通常写作8组,每组为四个十六进制数的形式,如 FE80:0000:0000:0000:AAAA:0000:00C2:0002。
IPv6套接字地址结构在<netinet/in.h>头文件中定义:
struct in6_addr
{
uint8_t s6_addr[16];
};
#define SIN6_LEN
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 # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
IPV6的地址族是AF_INET6,而IPv4的地址族是AF_INET。
结构中字段的先后顺序做过编排,使得如果sockaddr_in6的结构本身是64位对齐的,那么128位的sin6_addr字段也是64位对齐的。在一些64位处理机上,如果64位数据存储在某个64位边界位置,那么对它的访问将得到优化处理。
sin6_flowinfo字段分为两个字段:低序20位是流标(flow label),高序12位保留。
对于具备范围的地址(scoped address),sin6_scope_id字段标识其范围(scope),最常见的是链路局部地址(link-local address)的接口索引(interface index)
新的通用套接字地址结构:新的结构克服了sockaddr的一些缺点,新的sockaddr_storage足以容纳系统所支持的任何套接字地址结构。
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:
* 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.
*/
};
sockaddr_storage能够满足最苛刻的对齐要求。
sockaddr_storage足够大,可以容纳系统支持的任何套接字地址结构,除了ss_family和ss_len(如果有),其他的字段可以任意放置(对用户透明),sockaddr_storage结构必须强制转换成或复制到适合于ss_family字段所给出地址类型的套接字地址结构中,才能访问其他字段。
值-结果参数(说的是传递的参数作为返回结果的引用,eg, func(&res) ):
当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程的内核,还是从内核到进程。
1)从进程到内核传递套接字地址结构的函数有3个:bind、connect、sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小。(内核需要知道到底从进程复制了多少数据进来)
2)从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname和getpeername。这4个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针(这是一个结果,所以是引用传值)。
值-结果参数返回的结果:如果套接字地址结构是固定长度(如IPv4 (16) 和IPv6 (28) ),则返回值总是固定长度;对于可变长度(unix域等),返回值可能小于该结构的最大长度。
字节排序函数
小端字节序(little-endian):低序字节存储在起始地址,如0x12345678,在内存中从小到大的地址,存储序列是 78 56 34 12
大端字节序(big-endian):高序字节存储在起始地址,如0x12345678,在内存中从小到大的地址,存储序列是 12 34 56 78
以上两种格式都有系统使用!我们把某个给定系统所用的字节序成为主机字节序(host byte order)
#include "../unpv13e/unp.h"
#include "../unpv13e/apueerror.h"
// 以上路径是我自己的配置
// page.64 确定主机字节序的程序(小端对齐还是大端对齐)
int
main(int argc, char **argv)
{
union {
short s;
char c[sizeof(short)];
} un;
un.s = 0x0102;
printf("%s: ", CPU_VENDOR_OS); // 输出CPU类型、厂家和操作系统版本。
if (sizeof(short) == 2) {
if (un.c[0] == 1 && un.c[1] == 2)
printf("big-endian\n");
else if (un.c[0] == 2 && un.c[1] == 1)
printf("little-endian\n");
else
printf("unknown\n");
} else
printf("sizeof(short) = %d\n", (int)sizeof(short));
exit(0);
}
问题1:网络字节序和主机字节序的区别?
答:网际协议使用大端字节序来传送这些多字节整数,而系统使用的主机字节序可能是大端也可能是小端。
问题2:具体实现方法是怎样?
答:套接字地址结构的字段按照网络字节序(大端)进行维护,所以要通过函数进行转换。(以下h:host,n:network,s:short->16bits port,l:long->32bits ipv4)
htons 返回网络字节序的端口
htonl 返回网络字节序的ip
ntohs 返回主机字节序的端口
ntohl 返回主机字节序的ip
注意:事实上在64位系统中,长整数虽然占用64位,to long的函数操作的仍然是32位的值。
在大端字节序的系统中,这4个函数被定义为空宏。
字节操纵函数
处理字符串的函数被放在string.h中,然而像套接字地址结构这种多字节字段,需要全部清0,则需要用到字节操纵函数(有2组):
#include <strings.h>
// strings.h是从BSD系UNIX系统继承而来,里面定义了一些字符串函数,参考自 http://blog.csdn.net/xin_yu_xin/article/details/38672137
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); // 0为相等,非0为不相等
#include <string.h>
// ANSI C标准
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes); // 0为相同,非0为不相同
地址转换函数
作用:从点分十进制数串(如:206.168.112.96)转成网络字节序二进制值
两组函数:
(1) inet_aton , inet_addr , inet_ntoa (仅适用于IPv4)
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
// 若字符串有效则返回1,否则为0,。如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效性检查,但是不存储任何结果
in_addr_t inet_addr(const char *strptr);
// 字符串有效则返回32位二进制网络字节序的IPv4地址,否则返回INADDR_NONE(通常是255.255.255.255,这意味着这个有限广播地址不能由该函数来处理,还有一个问题是一些编译器编译的程序将返回-1的结果,而不是INADDR_NONE,所以这个函数现在已经被废弃,要用inet_aton或inet_pton来代替)
char *inet_ntoa(struct in_addr inaddr);
// 注意,参数是一个结构而不是一个结构指针(这是非常罕见的..),返回值是指向一个点分十进制数串的指针,该函数的返回值指向的字符串是驻留在静态内存中的,以为着该函数是不可重入的(后面的概念)
(2) inet_pton , inet_ntop (对IPv4和IPv6都适用) p for presentation(表达) n for numeric(数值)
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
// 成功则返回1,输入不是有效的表达格式则返回0,出错返回-1
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 成功则返回指向结果的指针,出错返回NULL,len是目标存储单元的大小,用于防止缓冲区溢出,为了有助于指定这个大小,在<netinet/in.h>头文件中定义了
// #define INET_ADDRSTRLEN 16
// #define INET6_ADDRSTRLEN 46
// 如果len太小,不足以容纳表达格式的结果(包括结尾的空字符),则返回一个空指针,置errno为ENOSPC,strptr参数不可以是一个空指针,必须先分配大小,调用成功时,这个指针就是该函数的返回值。
只支持IPv4的inet_pton和inet_ntop函数的简单定义:
int
inet_pton(int family, const char *strptr, void *addrptr)
{
if (family == AF_INET) {
struct in_addr in_val;
if (inet_aton(strptr, &in_val)) {
memcpy(addrptr, &in_val, sizeof(struct in_addr));
return (1);
}
return(0);
}
errno = EAFNOSUPPORT;
return (-1);
}
const char *
inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
{
const u_char *p = (const u_char *) addrptr;
if (family == AF_INET) {
char temp[INET_ADDRSTRLEN];
snprintf(temp, sizeof(temp), "%d.%d.%d.%d",
p[0], p[1], p[2], p[3]);
if (strlen(temp) >= len) {
errno = ENOSPC;
return (NULL);
}
strcpy(strptr, temp);
return (strptr);
}
errno = EAFNOSUPPORT;
return (NULL);
}
2.2 套接字中常用的函数
字节流套接字上调用read和write输入和输出的字节数可能比请求的数量要少,所以作者自己写了readn,writen和readline三个函数。
readn函数
#include "unp.h"
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 */
writen函数
#include "unp.h"
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);
}
readline函数
//较慢的一个版本,需要每次都调用系统的read函数
//作者还写了一个较快的版本,这里就不贴出代码了,主要思想是定义一个my_read一次读取进来,但每次都只返回给readline一个字符
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 = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
/* end readline */
2.3 基本TCP套接字编程
2.3.1、socket函数
#include <sys/socket.h>
int socket(int family,int type ,int protocol);
//返回:若成功则为非负描述符,若出错则为-1
其中family参数指明协议族,type参数指明套接字类型,proctocol参数为协议类型或者0
并非所有的套接字famliy和type都有效。
family | 说明 |
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密匙套接字 |
socket函数的type常值
ype | 说明 |
SOCK_STREM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
socket函数的protocol常数值
rotocol | 说明 |
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
2.3.2 connect函数
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
//返回:若成功则为0,若出错则为-1
sockfd是由socket函数返回的套接字描述符
第二个参数:一个指向套接字地址结构的指针
第三个参数:该结构体的大小
套接字的地址结构必须含有服务器IP地址和端口号
2.3.3 bind函数
bind函数把一个本地协议地址赋予一个套接字。
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
//返回:若成功则为0,若出错则为-1
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。
2.3.4 listen函数
isten函数仅由TCP服务器调用,它做两件事情:
1.当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受向该套接字的连接请求。调用listen函数导致套接字从closed状态转换到listen状态。
2.本函数的第二个参数规定了内核应该为相应的套接字排队的最大连接数
#include <sys/socket.h>
int listen(int sockfd,int backlog);
//返回:若成功则为0,若出错则为-1
本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
(1)未完成连接队列,每个这样的SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态
(2)已完成连接队列。每个已完成的TCP三次握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态。
2.3.5 accept函数
accept函数由服务器调用用于已完成的连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
//返回:若成功则为非负描述符,若出错则为-1
2.3.6 fork和exec函数
#include <unistd.h>
pid_t fork(void)
fork两个典型用法
1.一个进程创建一个自身副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是典型的网络服务器的用法。
2.一个进程想要执行另一个程序,既然创新进程的位置方法是调用fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常是子程序)调用exec把自身替换成新程序,这是诸如shell之类程序的典型用法。
#include <unistd.h>
int execl(const char *pathname,const char *arg0,..)
int execv(const char *pathname,char *const *argv[]);
int execle(const char *pathname,const char *arg0,...);
int execve(const char *pathname,char *const argv[],char *const envp[]);
2.3.7 并发服务器
unix中编写并发服务器程序的最简单的办法就是fork一个子进程来服务每个客户,
int
main(int argc, char **argv)
{
pid_t pid;
int listenfd, connfd;
socklen_t len;
struct sockaddr_in servaddr;
time_t ticks;
//创建套接字
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//初始化套接字
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;//IPv4协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//通配地址,一般为0
servaddr.sin_port = htons(13);//时间服务端口
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
if((pid = fork())==0)
{
close(listenfd);
doit(connfd);
close(connfd);
exit();
}
Close(connfd);
}
}
分析以上程序:
父进程:pid为子进程ID,不为0,则将connfd的引用套接字减1,父进程继续等待下一个客户连接子进程:fork函数之后,监听套接字和已连接套接字的引用技术都加1,pid==0,首先监听套接字listenfd的引用计数减1(不会关闭监听套接字),然后执行客户所需的操作(doit),再关闭connfd(引用计数减1,此时为0)。子进程处理客户需求结束,exit关闭进程。
2.3.8 close
用来关闭套接字,并中止TCP连接。
#include <unistd.h>
int close(int sockfd);/* 若成功则返回0,出错则返回-1*/
2.3.9 getsockname和getpreername
getsockname函数返回与某个套接字关联的本地协议地址,getpeername函数返回与某个套接字关联的外地协议地址。
#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);
需要使用上述函数的情况如下:
(1) 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号
(2) 在以端口0调用bind后,getsockname用于返回由内核赋予的本地端口号
(3) getsockname用于获取某个套接字的地址族
(4) 以通配IP地址调用bind的服务器上,与客户一旦建立连接,getsockname可用于返回由内核赋予该连接的本地IP地址
(5) 在一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它只能通过getpeername来获取客户的IP和端口号
2.4 套接字地址结构的长度之类的值-结果参数要用指针来传递原因
当函数调用时,结构大小是一个值,它告诉内核该结构大小,这样内核在写该结构时,不至于越界;当函数返回时,结构大小又是一个结果,他告诉进程内核在改结构中究竟 存储了多少信息。
值——结果参数,就是这么一种参数:传递的方向不同,导致其值和性质做改变。所谓传递的方向指的是从用户空间传递到内核,还是内核传递到用户空间。
当一个进程进行系统调用,把参数从用户空间传递到内核时,往往传递的是一个值,即按值传递。
当一个进程进行系统调用,把上次从用户空间传递到内核时的参数,从内核传递到用户空间时,传递的是指向该参数的指针,即按址传递。
这个”值——结果“参数用在套接字地址结构的相关函数中,往往会将一个套接字地址结构的指针和该结构的长度作为函数参数。
当这个函数是将该结构从用户空间传递到内核空间时,传递的长度是一个值,主要的目的是:由于有可能该结构是变长的,比如Unix,Datalink套接字地址结构就是变长的,告诉内核这个结构占用的最大的空间,如果对该结构进行写操作的话,不要进行地址越界了。而调用其他的函数对该套接字结构,进行从内核空间到用户空间的传递的话,这个长度参数是一个整型指针了,这个指针指向地址结构中的表示长度的成员。这个长度成员告诉了这个结构最终是用了多少空间。注意:此时的长度是内核自己进行赋值的,而不是用户操作的。