Linux 的套接字编程 (一)

本文深入探讨了TCP/IP协议族的基础知识,包括数据类型、函数定义、socket函数的使用方法,以及如何在客户端和服务器端进行连接、监听和接受连接。详细解释了sockaddr地址结构和各种地址族的定义,还提供了简单的客户端和服务器程序示例,帮助理解socket编程的基本概念。

一、需要的头文件

数据类型:#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是对于 UDPTCP 和其它 网间协议(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定义的前面:

  1. /* 
  2.  * 地址族 
  3.  */  
  4. #define AF_UNSPEC       0               /* 未指定 */  
  5. #define AF_LOCAL        1               /* 本机 (管道,portal) */  
  6. #define AF_UNIX         AF_LOCAL        /* 为了向前兼容 */  
  7. #define AF_INET         2               /* 网间协议: UDP, TCP, 等等 */  
  8. #define AF_IMPLINK      3               /* arpanet imp 地址 */  
  9. #define AF_PUP          4               /* pup 协议: 例如BSP */  
  10. #define AF_CHAOS        5               /* MIT CHAOS 协议 */  
  11. #define AF_NS           6               /* 施乐(XEROX) NS 协议 */  
  12. #define AF_ISO          7               /* ISO 协议 */  
  13. #define AF_OSI          AF_ISO  
  14. #define AF_ECMA         8               /* 欧洲计算机制造商协会 */  
  15. #define AF_DATAKIT      9               /* datakit 协议 */  
  16. #define AF_CCITT        10              /* CCITT 协议, X.25 等 */  
  17. #define AF_SNA          11              /* IBM SNA */  
  18. #define AF_DECnet       12              /* DECnet */  
  19. #define AF_DLI          13              /* DEC 直接数据链路接口 */  
  20. #define AF_LAT          14              /* LAT */  
  21. #define AF_HYLINK       15              /* NSC Hyperchannel */  
  22. #define AF_APPLETALK    16              /* Apple Talk */  
  23. #define AF_ROUTE        17              /* 内部路由协议 */  
  24. #define AF_LINK         18              /* 协路层接口 */  
  25. #define pseudo_AF_XTP   19              /* eXpress Transfer Protocol (no AF) */  
  26. #define AF_COIP         20              /* 面向连接的IP, 又名 ST II */  
  27. #define AF_CNT          21              /* Computer Network Technology */  
  28. #define pseudo_AF_RTIP  22              /* 用于识别RTIP包 */  
  29. #define AF_IPX          23              /* Novell 网间协议 */  
  30. #define AF_SIP          24              /* Simple 网间协议 */  
  31. #define pseudo_AF_PIP   25              /* 用于识别PIP包 */  
  32. #define AF_ISDN         26              /* 综合业务数字网(Integrated Services Digital Network) */  
  33. #define AF_E164         AF_ISDN         /* CCITT E.164 推荐 */  
  34. #define pseudo_AF_KEY   27              /* 内部密钥管理功能 */  
  35. #define AF_INET6        28              /* IPv6 */  
  36. #define AF_NATM         29              /* 本征ATM访问 */  
  37. #define AF_ATM          30              /* ATM */  
  38. #define pseudo_AF_HDRCMPLT 31           /* 由BPF使用,就不必在接口输出例程  
  39.                                          * 中重写头文件了  
  40.                                          */  
  41. #define AF_NETGRAPH     32              /* Netgraph 套接字 */  
  42. #define AF_SLOW         33              /* 802.3ad 慢速协议 */  
  43. #define AF_SCLUSTER     34              /* Sitara 集群协议 */  
  44. #define AF_ARP          35  
  45. #define AF_BLUETOOTH    36              /* 蓝牙套接字 */  
  46. #define AF_MAX          37  
 

 

用于指定IP的是 AF_INET。这个符号对应着常量 2

  在sockaddr中的域 sa_family指定地址族, 从而决定预先只确定下大致字节数的 sa_data的实际大小。

  特别是当地址族 是AF_INET时,我们可以使用 struct sockaddr_in,这可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作为实际替代。

  1. /* 
  2.  * 套接字地址,Internet风格 
  3.  */  
  4. struct sockaddr_in {  
  5.     uint8_t     sin_len;  
  6.     sa_family_t sin_family;  
  7.     in_port_t   sin_port;  
  8.     struct  in_addr sin_addr;  
  9.     char    sin_zero[8];  
  10. };  
 

 

三个重要的域是: 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之中:

  1. /* 
  2.  * Internet 地址 (由历史原因而形成的结构) 
  3.  */  
  4. struct in_addr {  
  5.     in_addr_t s_addr;  
  6. };  
 

 

in_addr_t是一个32位整数。

  假设地址192.43.244.18,这是为了表示32位整数的方便写法,按每个八位二进制字节列出, 以最高位的字节开始。

传入参数:

  1. sa.sin_family      = AF_INET;  
  2.     sa.sin_port        = 13;  
  3.     sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;  
 

在不同计算机上会产生不同的效果(所谓的Big Endian和Little Endian)

 

Big Endian - PowerPC,Sparc64,etc

Little Endian - X86

  1. 【用函数判断系统是Big Endian还是Little Endian】  
  2. bool IsBig_Endian()  
  3. //如果字节序为big-endian,返回true;  
  4. //反之为   little-endian,返回false  
  5. {  
  6.     unsigned short test = 0x1122;  
  7.     if(*( (unsigned char*) &test ) == 0x11)  
  8.        return TRUE;  
  9. else  
  10.     return FALSE;  
  11. }//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.huint32_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>

 

一旦一个客户端已经建立了一个套接字, 就需要把它连接到一个远方系统的一个端口上。

  1. 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的程序

  1. /* 
  2.  * daytime.c 
  3.  * 
  4.  * G. Adam Stanislav 编程 
  5.  */  
  6. #include <stdio.h>  
  7. #include <string.h>  
  8. #include <sys/types.h>  
  9. #include <sys/socket.h>  
  10. #include <netinet/in.h>  
  11. int main() {  
  12.   register int s;  
  13.   register int bytes;  
  14.   struct sockaddr_in sa;  
  15.   char buffer[BUFSIZ+1];  
  16.   if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {  
  17.     perror("socket");  
  18.     return 1;  
  19.   }  
  20.   bzero(&sa, sizeof sa);  
  21.   sa.sin_family = AF_INET;  
  22.   sa.sin_port = htons(13);  
  23.   sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);  
  24.   if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {  
  25.     perror("connect");  
  26.     close(s);  
  27.     return 2;  
  28.   }  
  29.   while ((bytes = read(s, buffer, BUFSIZ)) > 0)  
  30.     write(1, buffer, bytes);  
  31.   close(s);  
  32.   return 0;  
  33. }  
 

 

五、服务器函数

 

      典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。 服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿, 耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困, 所有的呼叫都同时来到。

  套接字接口提供三个基本的函数处理这种情况,bind,listen,accpet。

(1)bind函数

 

 我们使用bind函数 告诉套接字我们要服务的端口。

  1. int bind(int s, const struct sockaddr *addr, socklen_t addrlen);  
 

Sockfd:套接字描述符,指明创建连接的套接字

my_addr:本地地址,IP地址和端口号

addrlen :地址长度

 

(2)Listen函数

继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后, 现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。 还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。

  1. int listen(int s, int backlog);  

Sockfd:套接字描述符,指明创建连接的套接字

input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数 

 

(3)accept函数

 

在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。 这个连接保持到你或你的客户挂线。

  服务器通过使用函数accept函数接受连接。

 

 

 

  1. int accept(int s, struct sockaddr *addr, socklen_t *addrlen);  
 

注意,这次 addrlen 是一个指针。 这是必要的,因为在此情形中套接字要 填上 addr,这是一个 sockaddr_in 结构体。

  返回值是一个整数。其实, accept 返回一个 新 套接字。你将使用这个新套接字与客户通信。

  老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen的变量 backlog了吗?),直到我们 close(关闭) 它。

  现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen接受更多的连接。

 

Sockfd:套接字描述符,指明正在监听的套接字

addr:提出连接请求的主机地址

addrlen:地址长度

 

 

六、一个简单的服务器程序

 

  1. /* 
  2.  * daytimed - 端口 13 的服务器 
  3.  * 
  4.  * G. Adam Stanislav 编程 
  5.  * 2001年6月19日 
  6.  */  
  7. #include <stdio.h>  
  8. #include <string.h>  
  9. #include <time.h>  
  10. #include <unistd.h>  
  11. #include <sys/types.h>  
  12. #include <sys/socket.h>  
  13. #include <netinet/in.h>  
  14. #define BACKLOG 4  
  15. int main() {  
  16.     register int s, c;  
  17.     int b;  
  18.     struct sockaddr_in sa;  
  19.     time_t t;  
  20.     struct tm *tm;  
  21.     FILE *client;  
  22.     if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {  
  23.         perror("socket");  
  24.         return 1;  
  25.     }  
  26.     bzero(&sa, sizeof sa);  
  27.     sa.sin_family = AF_INET;  
  28.     sa.sin_port   = htons(13);  
  29.     if (INADDR_ANY)  
  30.         sa.sin_addr.s_addr = htonl(INADDR_ANY);  
  31.     if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {  
  32.         perror("bind");  
  33.         return 2;  
  34.     }  
  35.     switch (fork()) {  
  36.         case -1:  
  37.             perror("fork");  
  38.             return 3;  
  39.             break;  
  40.         default:  
  41.             close(s);  
  42.             return 0;  
  43.             break;  
  44.         case 0:  
  45.             break;  
  46.     }  
  47.     listen(s, BACKLOG);  
  48.     for (;;) {  
  49.         b = sizeof sa;  
  50.         if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {  
  51.             perror("daytimed accept");  
  52.             return 4;  
  53.         }  
  54.         if ((client = fdopen(c, "w")) == NULL) {  
  55.             perror("daytimed fdopen");  
  56.             return 5;  
  57.         }  
  58.         if ((t = time(NULL)) < 0) {  
  59.             perror("daytimed time");  
  60.             return 6;  
  61.         }  
  62.         tm = gmtime(&t);  
  63.         fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ/n",  
  64.             tm->tm_year + 1900,  
  65.             tm->tm_mon + 1,  
  66.             tm->tm_mday,  
  67.             tm->tm_hour,  
  68.             tm->tm_min,  
  69.             tm->tm_sec);  
  70.         fclose(client);  
  71.     }  
  72. }  
 

我们开始于建立一个套接字。然后我们填好 sockaddr_in 类型的结构体 sa。注意, INADDR_ANY的特定使用方法:

  1. if (INADDR_ANY)  
  2.         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 不是个总有许多客户请求的协议, 并且总可以立即处理每个请求。

  最后,守护进程开始无休止循环,按照如下步骤:

  1. 调用accept。 在这里等待直到一个客户端与之联系。在这里, 接收一个新套接字,c, 用来与其特定的客户通信。

  2. 使用 C 语言函数 fdopen 把套接字从一个 低级 文件描述符 转变成一个 C语言风格的 FILE 指针。 这使得后面可以使用 fprintf

  3. 检查时间,按 ISO 8601格式打印到 “文件” client。 然后使用 fclose 关闭文件。 这会把套接字一同自动关闭。

  我们可把这些步骤 概括 起来, 作为模型用于许多其它服务器:

 

 

 

 

 

这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。

  这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。

  注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。

  几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

  并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。

  这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。

  注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。

  几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

  并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值