1、Socket编程回顾套接字编程基础汇编Socket编程回顾(1)-套接字编程基础套接字,英文为socket,是一种双向的通信端口。位于网络中的主机通过连接的套接字提供的接口进行数据传输。本节将主要介绍使用套接字进行编程的一些基本概念。13.1.1 套接字与端口套接字是一种使用标准UNIX文件描述符(file descriptor)与其他程序通信的方式。套接字可以看作是处于不同主机之间的两个程序的通信连接端点。一方面程序将要传输的信息写入套接字中,而另一方面则通过读取套接字内的数据来获得传输的信息。图13.1 套接字通信示意图图13.1所示为使用套接字进行通信的示意图。假设存在两台主机A与B,在
2、主机A中存在进程C,主机B中存在进程D,当进程C需要将数据送到进程D时,首先将数据写到套接字中,而进程D可以通过读取套接字来获得进程C发送的信息。在网络中,不同计算机是通过IP地址来区分的,也就 是说,要将数据由主机A发送到主机B,只要知道主机B的IP地址就可以确定数据要发送的目的地。但是,在主机A与B中不可能只有进程C和进程D两个进程。 主机B在收到主机A发送来的数据后,如何才能确定该数据是发送给进程D?因此,还需要某种标识信息,用于描述网络通信数据发往的进程。TCP/IP协议提 出了协议端口的概念,用于标识通信的进程。当进程与某个端口绑定后,操作系统会将收到的给该端 口的数据送往该进程。与
3、文件描述符类似,每个端口都有被称为端口号的整数类型的标识符,该标识符用于区分不同的端口。不同协议可以使用相同的端口号进行数 据传输。例如,TCP使用了344的端口号,UDP同样可以使用344端口号进行数据传输。端口号为一个16位的无符号整数,其取值范围为065535。低于256的端口被作为系统的保留端口号,主要用于系统进程的通信,不在这一范围的端口号被称为自由端口号,可以由进程自由使用。13.1.2 套接字编程相关数据结构在开发使用套接字进行通信的程序时,常会用到sockaddr数据结构或sockaddr_in数据结构。sockaddr数据结构用于保存套接字的地址信息,具体定义如下:struc
4、t sockaddr unsigned short sa_family; char sa_data14;l sa_family:用于指定地址族,如果是TCP/IP通信,该值取PF_INET。l sa_data:用于保存套接字的IP地址和端口号信息。而sockaddr_in数据结构与sockaddr类似,该结构体的定义如下:struct sockaddr_in short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero8;l sin_family:用于指定地址
5、族。l sin_port:套接字通信的端口号。l sin_addr:通信的IP地址。l sin_zero8:用以填充0,保持与struct sockaddr同样大小。由于sockaddr数据结构与sockaddr_in数据结构的大小是相同的,指向sockaddr_in的指针可以通过强制转换,转换成指向sockaddr结构的指针。13.1.3 套接字类型常用的TCP/IP协议的3种套接字类型如下所示。l 流套接字(SOCK_STREAM):流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了
6、传输控制协议,即TCP(The Transmission Control Protocol)协议。l 数据报套接字(SOCK_DGRAM):数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据包套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。l 原始套接字(SOCK_RAW):原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字
7、可以读写内核没有 处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。13.1.4 big-endian与little-endian不同体系的CPU在内存中的数据存储往往存在着差 异。例如,Intel的x86系列处理器将低序字节存储在起始地址,而一些RISC架构的处理器,如IBM的370主机使用的PowerPC或 Motorola公司生产的CPU,都将高序字节存储在起始位置。这两种不同的存储方式被称为little-endian和big-endian。little- endian是x86系列CPU的数据存
8、储方式,即将低序的部分存储在前面。而big-endian是将高序部分存储在前面。例如,要存储 0xF432,little-endian将以32F4存储,而使用big-endian与此相反,将存储为F432,如图13.2所示。程序p13.1.c讲解了如何判断系统是使用big-endian还是little-endian实现数据存储的。程序中使用的方法如下所示。(1)利用联合的特点(不同变量共用同一内存)。联合中的数据成员是共享存储 空间的,所分配的空间为数据成员中最大所需的内存数。程序定义了名为endian_un的联合体,其中包含两个数据成员,一个是short类型的数据成员 (在32位系统上,sh
9、ort类型的长度是2字节),一个是字符类型的字符数组,字符数组的元素个数为short类型的字节数。程序将var赋值为0x0102。由于联合结构的特点,bits字符串数组中同样存储了0x0102这一数值。通过判断字符串中的低位和高位存储的内容,就可以知道系统是little-endian还是big-endian的。(2)通过强制类型转换实现。程序中通过取flag变量的地址,获得起始空间的存储内容。如果起始空间存储的是数据的低位内容,则表示存储方式为little-endian,否则为big-endian。程序的具体代码如下:/p13.1.c 判断big-endian与little-endian#in
10、clude /使用类型的强制转换实现little-endian与big-endian的判断int is_little_endian(void) unsigned short flag=0x4321; if(*(unsigned char*)&flag=0x21) return 1; else return 0;int main(void) /利用联合的特点来判断little-endian与big-endian union endian_un short var; char bitssizeof(short); ; union endian_un flag; flag.var=0x0102; /
11、判断低位和高位的存储内容,确定是何种方式 if(sizeof(short)=2) if(flag.bits0=1 & flag.bits1=2) printf(judged by first method, big-endiann); else if(flag.bits0=2 & flag.bits1=1) printf(judged by first method, little-endiann); else printf(cannot determine the typen); if(is_little_endian() printf(judged by second method, li
12、ttle-endiann); else printf(judged by second method, big-endiann); return 0;使用gcc编译p13.1.c,获得名为p13.1的可执行文件。执行该程序,具体输出如下。可以看到x86系统的内存数据存储方式为little-endian方式。programlocalhost charter13$ gcc -o p13.1 p13.1.cprogramlocalhost charter13$ ./p13.1judged by first method, little-endianjudged by second method, l
13、ittle-endianprogramlocalhost charter13$之所以介绍big-endian和 little-endian,是因为这一数据存储方式不仅影响程序在不同硬件平台中的移植,而且在网络编程中也要考虑字节顺序的问题。为了避免兼容性的问 题,网络中的数据传输都使用了从高到低的顺序存储方式。因此,如果要将数据从低位字节优先(little-endian)的机器上发往网络,必须首先进行 转换。而big-endian的机器是不需要转换的。Linux系统提供了htons、htonl、ntohs、ntoh这4个函数用于进行字节顺序的转换。其中,h是host的缩写,n表示network。
14、最后一个字符如果是s,表示short类型,如果是l,表示为long类型。4个函数的具体定义如下: uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);l htonl/htons:表示主机字节顺序转换成网络字节顺序,htonl函数和htons函数的区别在于参数长度存在差异。l ntohl/ntohs:表示网络字节顺序转换成主机字节顺序,ntohl函数和ntohs函数的区别在
15、于参数长度存在差异。上一节介绍了套接字有3种类型,其中流套接字可以实现可靠的数据传输。本节将介绍如何使用流套接字实现网络中主机间的通信。13.2.1 流套接字工作流程使用流套接字实现网络中不同主机间的通信属于典型的服务器/客户机模型,即客户端向服务器发送服务请求,服务器根据该请求提供相应的服务。图13.3所示为简单的通信示意图。为了实现服务器 与客户机间的通信,服务器和客户机都必须创建套接字。服务器在创建套接字后,需要指定监听的端口来等待客户机,因此,还有绑定端口号的操作。之后,服务器 将处于监听状态,等待客户机来连接指定端口。当接收到客户机的连接请求后,服务器调用accept函数来建立与客户
16、机间的通信。在成功建立通信后,就可以 通过read函数或write函数进行通信。客户端处的流程与服务端相比,简单的一些。客户端在创建套接字后,调用connect函数去连接服务器指定的端口。在服务器接收连接后,客户机与服务器之间就可以通过write函数和read函数实现数据通信了。图13.3 流套接字通信示意图下面将对图13.3中用于实现流套接字通信的函数进行详细介绍。13.2.2 socket函数socket函数的具体信息如表13.1所示。表13.1 socket函数头文件函数形式int socket(int domain, int type, int protocol);返回值成功失败是否设
17、置errno创建的socket的文件描述符1是说明:socket函数用于创建通信的套接字,并返回该套接字的文件描述符。参数domain指定了通信域,该参数用于选择通信协议族,其取值情况如表13.2所示。表13.2 domain取值情况表名 称含 义备 注PF_UNIX, PF_LOCAL本地通信“man 7 UNIX”可以获得具体帮助信息PF_INETIPv4协议“man 6 ip”可以获得具体帮助信息PF_INET6IPv6协议PF_IPXNovell公司的IPX协议PF_NETLINK与内核间的接口“man 7 netlink”可以获得具体帮助信息PF_X25ITU-T X.25 / IS
18、O-8208“man 7 x25”可以获得具体帮助信息PF_AX25无线AX.25协议PF_ATMPVC访问原始ATM的PVCPF_APPLETALK苹果公司的Appletalk协议“man 7 ddp”可以获得具体帮助信息PF_PACKET底层包接口“man 7 packet”获得具体帮助信息参数type用于指定套接字的类型。套接字类型除了前面提到的流套接字、数据报套接字及原始套接字外,还有其他的几种类型,如表13.3所示。表13.3 type参数可取值情况套接字类型说 明SOCK_STREAM提供有序、可靠、双向及基于连接的字节流。支持带外传输机制SOCK_DGRAM支持数据报SOCK_S
19、EQPACKET提供有序、可靠、双向基于连接的数据报通信SOCK_RAW提供对原始网络协议的访问SOCK_RDM提供可靠的数据报层,但是不保证有序性SOCK_PACKET该参数已经废除流套接字(SOCK_STREAM)与管道类似,是一种全双工的比特流。流套接字在发送或接收数据前必须处于连接状态。实现流套接字的通信协议保证了传输的数据不会丢失。参数protocol用于指定套接字使用的通信协议。正常情况下,对于给定的协议族,只有单一的协议支持特定的套接字类型。这时,只要将protocol参数设置为0即可。错误信息:EACCES:创建指定类型的套接字失败。EAFNOSUPPORT:不支持指定的地址族
20、。EINVAL:未知协议或未知的协议族。EMFILE:进程文件表溢出。ENFILE:达到打开文件的系统限制。ENOBUFS或ENOMEM:内存不足。EPROTONOSUPPORT:指定的协议类型在该域中不支持。13.2.3 bind函数bind函数用于将套接字与指定端口相连,其具体信息如表13.4所示。表13.4 bind函数头文件函数形式int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);返回值成功失败是否设置errno01是说明:当调用socket函数创建套接字后,该套接字并没 有与本机地址和端口等
21、信息相连,bind函数将完成这些工作。bind函数中的sockfd参数为调用socket函数后返回的文件描述符。 my_addr参数为指向sockaddr结构体的指针(该结构体中保存有端口和IP地址信息)。addlen参数为结构体sockaddr的长度。错误信息:EACCES:地址受到保护,用户非超级用户。EADDRINUSE:指定的地址已经在使用。EBADF:sockfd参数为非法的文件描述符。EINVAL:socket已经和地址绑定。ENOTSOCK:参数sockfd为文件描述符。实例演练:下面的代码来自于Linux系统的man帮助,注意创建套接字和使用bind函数实现socket的文件描
22、述符与地址信息绑定的过程。#include #include #include #include #define MY_SOCK_PATH /somepathintmain(int argc, char *argv) int sfd; struct sockaddr_un addr;/创建通信的套接字 sfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sfd = -1) perror(socket); exit(EXIT_FAILURE); /初始化addr变量 memset(&addr, 0, sizeof(struct sockaddr_un); add
23、r.sun_family = AF_UNIX; strncpy(addr.sun_path, MY_SOCK_PATH, sizeof(addr.sun_path) - 1);/将端口信息与套接字绑定 if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un) = -1) perror(bind); exit(EXIT_FAILURE); 13.2.4 listen函数服务器必须等待客户的连接请求,listen函数用于实现等待功能,该函数的具体信息如表13.5所示。表13.5 listen函数头文件函数形式int li
24、sten(int sockfd, int backlog);返回值成功失败是否设置errno01是说明:listen函数中,参数sockfd为调用socket函数获得的套接字的文件描述符信息。backlog参数为提出连接请求后,在服务器接收该连接请求时的等待队列中的连接数。默认情况,该值为20。系统调用listen只用于套接字类型为SOCK_STREAM或SOCK_SEQPACKET的场合。错误信息:EADDRINUSE:另一个socket也在监听同一个端口。EBADF:参数sockfd为非法的文件描述符。ENOTSOCK:参数sockfd不是文件描述符。EOPNOTSUPP:套接字类型不支持
25、listen操作。13.2.5 accept函数处于监听状态的服务器在获得客户机的连接请求后,会将其放置在等待队列中。当系统空闲时,将接受客户机的连接请求。接收客户机的连接请求使用accept函数,该函数的具体信息如表13.6所示。表13.6 accept函数头文件函数形式int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);返回值成功失败是否设置errno返回新的套接字文件描述符1是说明:accept函数用于面向连接类型的套接字类型 (SOCK_STREAM和SOCK_SEQPACKET)。accept函数将从连接
26、请求队列中获得连接信息,创建新的套接字,并返回该套接字的文件描 述符。新创建的套接字用于服务器与客户机的通信,而原来的套接字仍然处于监听状态。accept函数的sockfd参数为监听的套接字描述符。addr参数为指向结构体sockaddr的指针。参数addrlen为addr参数指向的内存空间的长度。错误信息:EAGAIN:套接字处于非阻塞状态,当前没有连接请求。EBADF:非法的文件描述符。ECONNABORTED:连接中断。EINTR:系统调用被信号中断。EINVAL:套接字没有处于监听状态,或非法的addrlen参数。EMFILE:达到进程打开文件描述符限制。ENFILE:达到打开文件数限
27、制。ENOTSOCK:文件描述符为文件的文件描述符。EOPNOTSUPP:套接字类型不是SOCK_STREAM。13.2.6 connect函数对于客户机而言,要与服务器进行通信,需要向服务器发出连接请求。connect函数用于完成这项功能,该函数的具体信息如表13.7所示。表13.7 connect函数头文件函数形式int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);返回值成功失败是否设置errno01是说明:connect函数将使用参数sockfd中的套接字连接到参数serv_addr中指
28、定的服务器。参数addrlen为serv_addr指向的内存空间大小。如果参数sockfd的类型为SOCK_DGRAM,serv_addr参数为数据报发往的地址,且将只接收该地址的数据报。如果sockfd的类型为SOCK_STREAM或SOCK_SEQPACKET,调用该函数将连接serv_addr中的服务器地址。错误信息:EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。EADDRINUSE:本地地址处于使用状态。EAFNOSUPPORT:参数serv_add中的地址非合法地址。EAGAIN:没有足够空闲的本地端口。EALREADY
29、:套接字为非阻塞套接字,并且原来的连接请求还未完成。EBADF:非法的文件描述符。ECONNREFUSED:远程地址并没有处于监听状态。EFAULT:指向套接字结构体的地址非法。EINPROGRESS:套接字为非阻塞套接字,且连接请求没有立即完成。EINTR:系统调用的执行由于捕获中断而中止。EISCONN:已经连接到该套接字。ENETUNREACH:网络不可到达。ENOTSOCK:文件描述符不与套接字相关。ETIMEDOUT:连接超时。13.2.7 发送与接收数据当服务器与客户机之间成功建立连接后,可以调用 read和write函数来实现对套接字的读写,以实现网络中不同主机间的通信。Linux系统还提供了send和recv函数,用于实现与read和 write函数相同的功能。而且send和recv的功能要比read函数和write函数更为全面。send函数的具体信息如表13.8所示。表13.8 send函数头文件函数形式ssize_t send(int s, const void *buf, size_t len, int flags);返回值成功失败是否设置errno返回实际发送的数据的字节数1是说明:send函数用于将信息发送到指定的套接字文件描述 符中。该函数只能用于已经建立连
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1