Linux网络编程 套接字
一:概述
Socket 的英文原意就是“孔”或“插座”,现在,作为BSD UNIX 的进程通讯机制,取其后一种意义。日常生活中常见的插座,有的是信号插座,有的是电源插座,有的可以接受信号(或能量),有的可以发送信号(或能量)。假如电话线与电话机之间安放一个插座(相当于二者之间的接口,这一部分装置物理上是存在的)则Socket 非常相似于电话插座。
将电话系统与面向连接的Socket 机制相比,有着惊人相似的地方。以一个国家级的电话网为例。电话的通话双方相当于相互通信的两个进程;通话双方所在的地区(享有一个全局唯一的区号)相当于一个网络,区号是它的网络地址;区内的一个单位的交换机相当于一台主机,主机分配给每个用户的局内号码相当于Socket 号(下面将谈到)。
任何用户在通话之前,首先要占有一部电话机,相当于申请一个Socket 号;同时要知道对方的电话号码,相当于对方有一个Socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是向电话机发出信号和从电话机接受信号的过程,相当于向Socket 发送数据和从Socket 接受数据。通话结束后,一方挂起电话机,相当于关闭Socket,撤消连接。
在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的过程、话音传输的过程以及整个电话系统的技术细节对它都是透明的,这也与Socket 机制非常相似。Socket 利用网间网通信设施实现进程通信,但它对通信设施的细节毫不关心,只要通信设施能提供足够的通信能力,它就满足了。
至此,我们对Socket 进行了直观的描述。抽象出来,Socket 实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。
二:套接字属性
套接字的特性由三个属性确定,它们是:域(domain),类型(type)和协议(protocol)。
1)套接字的域
域指定套接字通信中使用的网络 介质,最常见的套接字域是AF_INET,它指的是互联网络,许多LINUX局域网使用的都是该网络。
服务器上可能同时有多个服务正在运行,客户可以通过IP端口来指定一台联网机器上的某个特定服务。在系统内部,端口通过分配一个唯一的16位整数来标识,在系统外部,则需要通过IP地址和端口号的组合来确定。套接字作为通信的终点,它必须在开始通信之前绑定一个端口。
服务器在特定的端口等待客户的连接,知名服务器所分配的端口号在所有LINUX和UNIX机器上都是一样的,它们通常(但并不总是如此)小于1024,一般情况下,小于1024的端口号都是为系统服务保留的,并且所服务的进程必须具有超级用户权限。
其它可以使用的域如AF_UNIX,AF_ISO,AF_XFA等。
2)套接字类型
套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字。
1 流式套接字(SOCK_STREAM)
流式套接字提供的是一个有序,可靠,双向字节流的连接。因此,发送的数据可以确保不会丢失,复制或乱序到达,并且在这一过程 中发生的错误也不会显示出来。流式套接字由类型SOCK_STREAM指定,它们是地AF_INET域中通过TCP/IP连接来实现的,它们也是AF_UNIX域中常用的套接字类型。
2 数据报套接字(SOCK_DGRAM)
与此相反 ,由SOCK_DGRAM指定的数据报套接字不建立和维持一个连接,它寻可以发送的数据报的长度有限制。数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序到达。数据报套接字是在AF_INET域中通过UDP/IP连接来实现的,它提供的是一种无序的不可靠服务,但从资源的角度来看,相对来说它们开销比较小,因为不需要维持网络连接,而且因为无需花费时间来建立连接,它们的速度也很快。
3.原始套接字
现在用得很少了,参看下面这篇文章:
http://soft.zdnet.com.cn/software_zone/2007/1020/568223.shtml
三 :套接字协议
只要底层传输机制允许不止一个协议来提供所要求的套接字类型,就可以为套接字选择一个特定的协议。我们重点讨论UNIX网络套接字和文件系统套接字,它们不需要你 选择一个特定的协议,只需要使用其默认值即可。
四:创建套接字
socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。
#include
#include
socket函数原型为:
int socket(int domain, int type, int protocol);
domain参数指定socket的类型,一般为AF_INET,type可以是SOCK_STREAM 或SOCK_DGRAM,分别表示TCP连接和UDP连接;protocol通常赋值"0"。Socket()调用返回一个整型socket描述符,你可 以在后面的调用使用它。
一旦通过socket调用返回一个socket描述符,你应该将该socket与你本机上的一个端口相关联(往往当你在设计服务器端程序时需要调用该函数。随后你就可以在该端口监听服务请求;而客户端一般无须调用该函数)。
五:套接字地址
每个套接字域都有其自己的地址格式。对于AF_INET域来说,套接字地址由结构sockaddr_in来指定,该结构在头文件netinet/in.h中定义。
我们要讨论的第一个结构类型是:struct sockaddr,该类型是用来保存socket信息的:
struct sockaddr {
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址 */
};
sa_family一般为AF_INET;sa_data则包含该socket的IP地址和端口号。
另外还有一种结构类型:
struct sockaddr_in {
short int sin_ family; /* 地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */
};
这个结构使用更为方便。sin_zero(它用来将sockaddr_in结构填充到与struct sockaddr同样的长度)应该用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。sin_family通常被赋AF_INET;sin_port和 sin_addr应该转换成为网络字节优先顺序 。
IP地址结构struct in_addr被定义为:
struct in_addr{
unsigned long int s_addr;
};
IP地址中的四个字节组成一个32位的值。
我们下面讨论几个字节顺序转换函数:
htons()--"Host to Network Short" ; htonl()--"Host to Network Long"
ntohs()--"Network to Host Short" ; ntohl()--"Network to Host Long"
在这里,h表示"host" ,n表示"network",s 表示"short",l表示 "long"。
六:命名套接字
Bind函数原型为:
#include
int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
Sockfd是一个socket描述符,my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;addrlen常被设置为sizeof(struct sockaddr)。
bind系统调用把参数my_addr中的地址分配给与文件描述符sockfd关联的未命名套接字,地址的长度由参数addrlen传递。bind系统调用需要将一个特定地址结构指针转换为指向通用地址类型(struct sockaddr *)。
最后,对于bind 函数要说明的一点是,你可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:
my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机IP地址 */
通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将 my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。Bind()函数在成功被调用时返回0;遇到错 误时返回"-1"并将errno置为相应的错误号。另外要注意的是,当调用函数时,一般不要将端口号置为小于1024的值,因为1~1024是保留端口号 ,你可以使用大于1024中任何一个没有被占用的端口号。
七:创建套接字队列
为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求,它用listen系统调用来完成这一工作:
Listen()——监听是否有服务请求
在服务器端程序中,当socket与某一端口捆绑以后,就需要监听该端口,以便对到达的服务请求加以处理。
#include
int listen(int sockfd, int backlog);
Sockfd是Socket系统调用返回的socket 描述符;backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待accept()它们(参考下文)。Backlog对队列中等待 服务的请求的数目进行了限制,大多数系统缺省值为20,backlog参数常用的值是5。当listen遇到错误时返回-1,errno被置为相应的错误码。
八:接受连接
#include
accept()——连接端口的服务请求。
当某个客户端试图与服务器监听的端口连接时,该连接请求将排队等待服务器 accept()它 。通过调用accept()函数为其建立一个连接,accept()函数将返回一个新的socket描述符,来供这个新连接来使用。而 服务器可以继续在以前的那个 socket上监听,同时可以在新的socket描述符上进行数据send()(发送)和recv()(接收)操作:
int accept(int sockfd, void *addr, int *addrlen);
sockfd是被监听的socket描述符,addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息 (某台主机从某个端口发出该请求);addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。错误发生时返回一个-1并且设置相应的errno值。
九:请求连接:
在客户端,客户程序通过一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器,它们通过connect调用来完成这一工作。
Connect()函数用来与远端服务器建立一个TCP连接,其函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
Sockfd是目的服务器的sockt描述符;serv_addr是包含目的机IP地址和端口号的指针。遇到错误时返回-1,并且errno中包含相应 的错误码。进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,内核 会自动选择一个未被占用的端口供客户端来使用。
十:数据传输
Send()和recv()——数据传输
这两个函数是用于面向连接的socket上进行数据传输。