一、需要的头文件
数据类型:#include <sys/types.h>
函数定义:#include <sys/socket.h>
TCP/IP协议族:PF_INET
TCP/IP的地址族:AF_INET
二、socke函数
int socket(int domain, int type, int protocol);
这一个函数在客户端和服务器都要使用。 它是这样被声明的:
返回值的类型与open
的相同,一个整数。 FreeBSD从和文件句柄相同的池中分配它的值。 这就是允许套接字被以对文件相同的方式处理的原因。
(1)参数domain
告诉系统你需要使用什么 协议族。有许多种协议族存在,有些是某些厂商专有的, 其它的都非常通用。协议族的声明在sys/socket.h中
使用PF_INET
是对于 UDP, TCP 和其它 网间协议(IPv4)的情况。
(2)对于参数type
有五个定义好的值,也在 sys/socket.h中。这些值都以 “SOCK_
”开头。 其中最通用的是SOCK_STREAM
, 它告诉系统你正需要一个可靠的流传送服务 (和PF_INET
一起使用时是指 TCP)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
如果指定SOCK_DGRAM
, 你是在请求无连接报文传送服务 (在我们的情形中是UDP)数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。。
如何你需要处理基层协议 (例如IP),对较低层次协议,如IP、ICMP直接访问或者甚至是网络接口 (例如,以太网),你就需要指定 SOCK_RAW
。
(3)参数protocol
取决于前两个参数, 并非总是有意义。在以上情形中,使用取值0
。
三、Sockaddr 地址结构解析
各种各样的套接字函数需要指定地址,那是一小块内存空间 (用C语言术语是指向一小块内存空间的指针)。在 sys/socket.h中有各种各样如struct sockaddr
的声明。 这个结构是这样被声明的:
/* * 内核用来存储大多数种类地址的结构 */ struct sockaddr { u_char sa_len; /* 总长度 */ u_short sa_family; /* 地址族 */ char sa_data[14]; /* 地址值,实际可能更长 */ }; #define SOCK_MAXADDRLEN 255 /* 可能的最长的地址长度 */
sys/socket.h提到的各种类型的协议 将被按照地址族对待,并把它们就列在 sockaddr
定义的前面:
- /*
- * 地址族
- */
- #define AF_UNSPEC 0 /* 未指定 */
- #define AF_LOCAL 1 /* 本机 (管道,portal) */
- #define AF_UNIX AF_LOCAL /* 为了向前兼容 */
- #define AF_INET 2 /* 网间协议: UDP, TCP, 等等 */
- #define AF_IMPLINK 3 /* arpanet imp 地址 */
- #define AF_PUP 4 /* pup 协议: 例如BSP */
- #define AF_CHAOS 5 /* MIT CHAOS 协议 */
- #define AF_NS 6 /* 施乐(XEROX) NS 协议 */
- #define AF_ISO 7 /* ISO 协议 */
- #define AF_OSI AF_ISO
- #define AF_ECMA 8 /* 欧洲计算机制造商协会 */
- #define AF_DATAKIT 9 /* datakit 协议 */
- #define AF_CCITT 10 /* CCITT 协议, X.25 等 */
- #define AF_SNA 11 /* IBM SNA */
- #define AF_DECnet 12 /* DECnet */
- #define AF_DLI 13 /* DEC 直接数据链路接口 */
- #define AF_LAT 14 /* LAT */
- #define AF_HYLINK 15 /* NSC Hyperchannel */
- #define AF_APPLETALK 16 /* Apple Talk */
- #define AF_ROUTE 17 /* 内部路由协议 */
- #define AF_LINK 18 /* 协路层接口 */
- #define pseudo_AF_XTP 19 /* eXpress Transfer Protocol (no AF) */
- #define AF_COIP 20 /* 面向连接的IP, 又名 ST II */
- #define AF_CNT 21 /* Computer Network Technology */
- #define pseudo_AF_RTIP 22 /* 用于识别RTIP包 */
- #define AF_IPX 23 /* Novell 网间协议 */
- #define AF_SIP 24 /* Simple 网间协议 */
- #define pseudo_AF_PIP 25 /* 用于识别PIP包 */
- #define AF_ISDN 26 /* 综合业务数字网(Integrated Services Digital Network) */
- #define AF_E164 AF_ISDN /* CCITT E.164 推荐 */
- #define pseudo_AF_KEY 27 /* 内部密钥管理功能 */
- #define AF_INET6 28 /* IPv6 */
- #define AF_NATM 29 /* 本征ATM访问 */
- #define AF_ATM 30 /* ATM */
- #define pseudo_AF_HDRCMPLT 31 /* 由BPF使用,就不必在接口输出例程
- * 中重写头文件了
- */
- #define AF_NETGRAPH 32 /* Netgraph 套接字 */
- #define AF_SLOW 33 /* 802.3ad 慢速协议 */
- #define AF_SCLUSTER 34 /* Sitara 集群协议 */
- #define AF_ARP 35
- #define AF_BLUETOOTH 36 /* 蓝牙套接字 */
- #define AF_MAX 37
用于指定IP的是 AF_INET
。这个符号对应着常量 2
。
在sockaddr
中的域 sa_family
指定地址族, 从而决定预先只确定下大致字节数的 sa_data
的实际大小。
特别是当地址族 是AF_INET
时,我们可以使用 struct sockaddr_in
,这可在 netinet/in.h中找到,任何需要 sockaddr
的地方都以此作为实际替代。
- /*
- * 套接字地址,Internet风格
- */
- struct sockaddr_in {
- uint8_t sin_len;
- sa_family_t sin_family;
- in_port_t sin_port;
- struct in_addr sin_addr;
- char sin_zero[8];
- };
三个重要的域是: sin_family
,结构体的字节1 1B; sin_port
,16位值,在字节2和3 2B; sin_addr
,一个32位整数,表示 IP地址,存储在字节4-7 4B。
域sin_addr
被声明为类型 struct in_addr
,这个类型定义在 netinet/in.h之中:
- /*
- * Internet 地址 (由历史原因而形成的结构)
- */
- struct in_addr {
- in_addr_t s_addr;
- };
而in_addr_t
是一个32位整数。
假设地址192.43.244.18,这是为了表示32位整数的方便写法,按每个八位二进制字节列出, 以最高位的字节开始。
传入参数:
- sa.sin_family = AF_INET;
- sa.sin_port = 13;
- sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;
在不同计算机上会产生不同的效果(所谓的Big Endian和Little Endian)
Big Endian - PowerPC,Sparc64,etc
Little Endian - X86
- 【用函数判断系统是Big Endian还是Little Endian】
- bool IsBig_Endian()
- //如果字节序为big-endian,返回true;
- //反之为 little-endian,返回false
- {
- unsigned short test = 0x1122;
- if(*( (unsigned char*) &test ) == 0x11)
- return TRUE;
- else
- return FALSE;
- }//IsBig_Endian()
所有网络协议都是采用Big Endian的方式来传输数据的,而Intel X86主机采用的是Little Endian,所以我们需要注意这一点
需要使用对应的转换函数
IP地址转换函数
inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址
inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址
字节排序函数
#include <arpa/inet.h> or #include <netinet/in.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_thostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
三、客户端函数
(1)connect函数
需要头文件
#include <sys/types.h> #include <sys/socket.h>
一旦一个客户端已经建立了一个套接字, 就需要把它连接到一个远方系统的一个端口上。
- int connect(int s, const struct sockaddr *name, socklen_t namelen);
参数 s
是套接字, 那是由函数socket
返回的值。 name
是一个指向 sockaddr
的指针,这个结构体我们已经展开讨论过了。 最后,namelen
通知系统 在我们的sockaddr
结构体中有多少字节。
如果 connect
成功, 返回 0
。否则返回 -1
并将错误码存放于 errno
之中。
connect函数是阻塞模式函数,除非接受到相关数据否则一直等待,类似的函数还有recvfrom和recv函数
四、一个简单的客户端程序, 一个从192.43.244.18获取当前时间并打印到 stdout的程序
- /*
- * daytime.c
- *
- * G. Adam Stanislav 编程
- */
- #include <stdio.h>
- #include <string.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- int main() {
- register int s;
- register int bytes;
- struct sockaddr_in sa;
- char buffer[BUFSIZ+1];
- if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
- perror("socket");
- return 1;
- }
- bzero(&sa, sizeof sa);
- sa.sin_family = AF_INET;
- sa.sin_port = htons(13);
- sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);
- if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
- perror("connect");
- close(s);
- return 2;
- }
- while ((bytes = read(s, buffer, BUFSIZ)) > 0)
- write(1, buffer, bytes);
- close(s);
- return 0;
- }
五、服务器函数
典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。 服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿, 耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困, 所有的呼叫都同时来到。
套接字接口提供三个基本的函数处理这种情况,bind,listen,accpet。
(1)bind函数
我们使用bind函数 告诉套接字我们要服务的端口。
- int bind(int s, const struct sockaddr *addr, socklen_t addrlen);
Sockfd:套接字描述符,指明创建连接的套接字
my_addr:本地地址,IP地址和端口号
addrlen :地址长度
(2)Listen函数
继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后, 现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。 还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。
- int listen(int s, int backlog);
Sockfd:套接字描述符,指明创建连接的套接字
input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
(3)accept函数
在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。 这个连接保持到你或你的客户挂线。
服务器通过使用函数accept函数接受连接。
- int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
注意,这次 addrlen
是一个指针。 这是必要的,因为在此情形中套接字要 填上 addr
,这是一个 sockaddr_in
结构体。
返回值是一个整数。其实, accept
返回一个 新 套接字。你将使用这个新套接字与客户通信。
老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen
的变量 backlog
了吗?),直到我们 close
(关闭) 它。
现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen
接受更多的连接。
Sockfd:套接字描述符,指明正在监听的套接字
addr:提出连接请求的主机地址
addrlen:地址长度
六、一个简单的服务器程序
- /*
- * daytimed - 端口 13 的服务器
- *
- * G. Adam Stanislav 编程
- * 2001年6月19日
- */
- #include <stdio.h>
- #include <string.h>
- #include <time.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #define BACKLOG 4
- int main() {
- register int s, c;
- int b;
- struct sockaddr_in sa;
- time_t t;
- struct tm *tm;
- FILE *client;
- if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
- perror("socket");
- return 1;
- }
- bzero(&sa, sizeof sa);
- sa.sin_family = AF_INET;
- sa.sin_port = htons(13);
- if (INADDR_ANY)
- sa.sin_addr.s_addr = htonl(INADDR_ANY);
- if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
- perror("bind");
- return 2;
- }
- switch (fork()) {
- case -1:
- perror("fork");
- return 3;
- break;
- default:
- close(s);
- return 0;
- break;
- case 0:
- break;
- }
- listen(s, BACKLOG);
- for (;;) {
- b = sizeof sa;
- if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {
- perror("daytimed accept");
- return 4;
- }
- if ((client = fdopen(c, "w")) == NULL) {
- perror("daytimed fdopen");
- return 5;
- }
- if ((t = time(NULL)) < 0) {
- perror("daytimed time");
- return 6;
- }
- tm = gmtime(&t);
- fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ/n",
- tm->tm_year + 1900,
- tm->tm_mon + 1,
- tm->tm_mday,
- tm->tm_hour,
- tm->tm_min,
- tm->tm_sec);
- fclose(client);
- }
- }
我们开始于建立一个套接字。然后我们填好 sockaddr_in
类型的结构体 sa
。注意, INADDR_ANY
的特定使用方法:
- if (INADDR_ANY)
- sa.sin_addr.s_addr = htonl(INADDR_ANY);
这个常量的值是0
。由于我们已经使用 bzero
于整个结构体, 再把成员设为0
将是冗余。 但是如果我们把代码移植到其它一些 INADDR_ANY
可能不是0的系统上, 我们就需要把实际值指定给 sa.sin_addr.s_addr
。多数现在C语言 编译器已足够智能,会注意到 INADDR_ANY
是一个常量。由于它是0, 他们将会优化那段代码外的整个条件语句。
在我们成功调用bind
后, 我们已经准备好成为一个 守护进程:我们使用 fork
建立一个子进程。 同在父进程和子进程里,变量s
都是套接字。 父进程不再需要它,于是调用了close
, 然后返回0
通知父进程的父进程成功终止。
此时,子进程继续在后台工作。 它调用listen
并设置 backlog 为 4
。这里并不需要设置一个很大的值, 因为 daytime 不是个总有许多客户请求的协议, 并且总可以立即处理每个请求。
最后,守护进程开始无休止循环,按照如下步骤:
-
调用
accept
。 在这里等待直到一个客户端与之联系。在这里, 接收一个新套接字,c
, 用来与其特定的客户通信。 -
使用 C 语言函数
fdopen
把套接字从一个 低级 文件描述符 转变成一个 C语言风格的FILE
指针。 这使得后面可以使用fprintf
。 -
检查时间,按 ISO 8601格式打印到 “文件”
client
。 然后使用fclose
关闭文件。 这会把套接字一同自动关闭。
我们可把这些步骤 概括 起来, 作为模型用于许多其它服务器:
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork
之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal
处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork
之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal
处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。