Socket编程回顾套接字编程基础汇编.docx
《Socket编程回顾套接字编程基础汇编.docx》由会员分享,可在线阅读,更多相关《Socket编程回顾套接字编程基础汇编.docx(25页珍藏版)》请在冰豆网上搜索。
![Socket编程回顾套接字编程基础汇编.docx](https://file1.bdocx.com/fileroot1/2023-2/6/b81eeb94-d398-4626-9148-b68aa9bef36d/b81eeb94-d398-4626-9148-b68aa9bef36d1.gif)
Socket编程回顾套接字编程基础汇编
Socket编程回顾
(1)--套接字编程基础
套接字,英文为socket,是一种双向的通信端口。
位于网络中的主机通过连接的套接字提供的接口进行数据传输。
本节将主要介绍使用套接字进行编程的一些基本概念。
13.1.1套接字与端口
套接字是一种使用标准UNIX文件描述符(filedescriptor)与其他程序通信的方式。
套接字可以看作是处于不同主机之间的两个程序的通信连接端点。
一方面程序将要传输的信息写入套接字中,而另一方面则通过读取套接字内的数据来获得传输的信息。
图13.1套接字通信示意图
图13.1所示为使用套接字进行通信的示意图。
假设存在两台主机A与B,在主机A中存在进程C,主机B中存在进程D,当进程C需要将数据送到进程D时,首先将数据写到套接字中,而进程D可以通过读取套接字来获得进程C发送的信息。
在网络中,不同计算机是通过IP地址来区分的,也就是说,要将数据由主机A发送到主机B,只要知道主机B的IP地址就可以确定数据要发送的目的地。
但是,在主机A与B中不可能只有进程C和进程D两个进程。
主机B在收到主机A发送来的数据后,如何才能确定该数据是发送给进程D?
因此,还需要某种标识信息,用于描述网络通信数据发往的进程。
TCP/IP协议提出了协议端口的概念,用于标识通信的进程。
当进程与某个端口绑定后,操作系统会将收到的给该端口的数据送往该进程。
与文件描述符类似,每个端口都有被称为端口号的整数类型的标识符,该标识符用于区分不同的端口。
不同协议可以使用相同的端口号进行数据传输。
例如,TCP使用了344的端口号,UDP同样可以使用344端口号进行数据传输。
端口号为一个16位的无符号整数,其取值范围为0~65535。
低于256的端口被作为系统的保留端口号,主要用于系统进程的通信,不在这一范围的端口号被称为自由端口号,可以由进程自由使用。
13.1.2套接字编程相关数据结构
在开发使用套接字进行通信的程序时,常会用到sockaddr数据结构或sockaddr_in数据结构。
sockaddr数据结构用于保存套接字的地址信息,具体定义如下:
structsockaddr{
unsignedshortsa_family;
charsa_data[14]
};
lsa_family:
用于指定地址族,如果是TCP/IP通信,该值取PF_INET。
lsa_data:
用于保存套接字的IP地址和端口号信息。
而sockaddr_in数据结构与sockaddr类似,该结构体的定义如下:
structsockaddr_in{
shortintsin_family;
unsignedshortintsin_port;
structin_addrsin_addr;
unsignedcharsin_zero[8];
};
lsin_family:
用于指定地址族。
lsin_port:
套接字通信的端口号。
lsin_addr:
通信的IP地址。
lsin_zero[8]:
用以填充0,保持与structsockaddr同样大小。
由于sockaddr数据结构与sockaddr_in数据结构的大小是相同的,指向sockaddr_in的指针可以通过强制转换,转换成指向sockaddr结构的指针。
13.1.3套接字类型
常用的TCP/IP协议的3种套接字类型如下所示。
l流套接字(SOCK_STREAM):
流套接字用于提供面向连接、可靠的数据传输服务。
该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。
流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(TheTransmissionControlProtocol)协议。
l数据报套接字(SOCK_DGRAM):
数据报套接字提供了一种无连接的服务。
该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
数据报套接字使用UDP(UserDatagramProtocol)协议进行数据的传输。
由于数据包套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
l原始套接字(SOCK_RAW):
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。
因此,如果要访问其他协议发送数据必须使用原始套接字。
13.1.4big-endian与little-endian
不同体系的CPU在内存中的数据存储往往存在着差异。
例如,Intel的x86系列处理器将低序字节存储在起始地址,而一些RISC架构的处理器,如IBM的370主机使用的PowerPC或Motorola公司生产的CPU,都将高序字节存储在起始位置。
这两种不同的存储方式被称为little-endian和big-endian。
little-endian是x86系列CPU的数据存储方式,即将低序的部分存储在前面。
而big-endian是将高序部分存储在前面。
例如,要存储0xF432,little-endian将以32F4存储,而使用big-endian与此相反,将存储为F432,如图13.2所示。
程序p13.1.c讲解了如何判断系统是使用big-endian还是little-endian实现数据存储的。
程序中使用的方法如下所示。
(1)利用联合的特点(不同变量共用同一内存)。
联合中的数据成员是共享存储空间的,所分配的空间为数据成员中最大所需的内存数。
程序定义了名为endian_un的联合体,其中包含两个数据成员,一个是short类型的数据成员(在32位系统上,short类型的长度是2字节),一个是字符类型的字符数组,字符数组的元素个数为short类型的字节数。
程序将var赋值为0x0102。
由于联合结构的特点,bits字符串数组中同样存储了0x0102这一数值。
通过判断字符串中的低位和高位存储的内容,就可以知道系统是little-endian还是big-endian的。
(2)通过强制类型转换实现。
程序中通过取flag变量的地址,获得起始空间的存储内容。
如果起始空间存储的是数据的低位内容,则表示存储方式为little-endian,否则为big-endian。
程序的具体代码如下:
//p13.1.c判断big-endian与little-endian
#include
//使用类型的强制转换实现little-endian与big-endian的判断
intis_little_endian(void)
{
unsignedshortflag=0x4321;
if(*(unsignedchar*)&flag==0x21)
return1;
else
return0;
}
intmain(void)
{
//利用联合的特点来判断little-endian与big-endian
unionendian_un{
shortvar;
charbits[sizeof(short)];
};
unionendian_unflag;
flag.var=0x0102;
//判断低位和高位的存储内容,确定是何种方式
if(sizeof(short)==2){
if(flag.bits[0]==1&&flag.bits[1]==2)
printf("judgedbyfirstmethod,big-endian\n");
elseif(flag.bits[0]==2&&flag.bits[1]==1)
printf("judgedbyfirstmethod,little-endian\n");
else
printf("cannotdeterminethetype\n");
}
if(is_little_endian())
printf("judgedbysecondmethod,little-endian\n");
else
printf("judgedbysecondmethod,big-endian\n");
return0;
}
使用gcc编译p13.1.c,获得名为p13.1的可执行文件。
执行该程序,具体输出如下。
可以看到x86系统的内存数据存储方式为little-endian方式。
[program@localhostcharter13]$gcc-op13.1p13.1.c
[program@localhostcharter13]$./p13.1
judgedbyfirstmethod,little-endian
judgedbysecondmethod,little-endian
[program@localhostcharter13]$
之所以介绍big-endian和little-endian,是因为这一数据存储方式不仅影响程序在不同硬件平台中的移植,而且在网络编程中也要考虑字节顺序的问题。
为了避免兼容性的问题,网络中的数据传输都使用了从高到低的顺序存储方式。
因此,如果要将数据从低位字节优先(little-endian)的机器上发往网络,必须首先进行转换。
而big-endian的机器是不需要转换的。
Linux系统提供了htons、htonl、ntohs、ntoh这4个函数用于进行字节顺序的转换。
其中,h是host的缩写,n表示network。
最后一个字符如果是s,表示short类型,如果是l,表示为long类型。
4个函数的具体定义如下:
uint32_thtonl(uint32_thostlong);
uint16_thtons(uint16_thostshort);
uint32_tntohl(uint32_tnetlong);
uint16_tntohs(uint16_tnetshort);
lhtonl/htons:
表示主机字节顺序转换成网络字节顺序,htonl函数和htons函数的区别在于参数长度存在差异。
lntohl/ntohs:
表示网络字节顺序转换成主机字节顺序,ntohl函数和ntohs函数的区别在于参数长度存在差异。
上一节介绍了套接字有3种类型,其中流套接字可以实现可靠的数据传输。
本节将介绍如何使用流套接字实现网络中主机间的通信。
13.2.1流套接字工作流程
使用流套接字实现网络中不同主机间的通信属于典型的服务器/客户机模型,即客户端向服务器发送服务请求,服务器根据该请求提供相应的服务。
图13.3所示为简单的通信示意图。
为了实现服务器与客户机间的通信,服务器和客户机都必须创建套接字。
服务器在创建套接字后,需要指定监听的端口来等待客户机,因此,还有绑定端口号的操作。
之后,服务器将处于监听状态,等待客户机来连接指定端口。
当接收到客户机的连接请求后,服务器调用accept函数来建立与客户机间的通信。
在成功建立通信后,就可以通过read函数或write函数进行通信。
客户端处的流程与服务端相比,简单的一些。
客户端在创建套接字后,调用connect函数去连接服务器指定的端口。
在服务器接收连接后,客户机与服务器之间就可以通过write函数和read函数实现数据通信了。
图13.3流套接字通信示意图
下面将对图13.3中用于实现流套接字通信的函数进行详细介绍。
13.2.2socket函数
socket函数的具体信息如表13.1所示。
表13.1socket函数
头文件
函数形式
intsocket(intdomain,inttype,intprotocol);
返回值
成功
失败
是否设置errno
创建的socket的文件描述符
−1
是
说明:
socket函数用于创建通信的套接字,并返回该套接字的文件描述符。
参数domain指定了通信域,该参数用于选择通信协议族,其取值情况如表13.2所示。
表13.2domain取值情况表
名称
含义
备注
PF_UNIX,PF_LOCAL
本地通信
“man7UNIX”可以获得具体帮助信息
PF_INET
IPv4协议
“man6ip”可以获得具体帮助信息
PF_INET6
IPv6协议
−
PF_IPX
Novell公司的IPX协议
−
PF_NETLINK
与内核间的接口
“man7netlink”可以获得具体帮助信息
PF_X25
ITU-TX.25/ISO-8208
“man7x25”可以获得具体帮助信息
PF_AX25
无线AX.25协议
−
PF_ATMPVC
访问原始ATM的PVC
−
PF_APPLETALK
苹果公司的Appletalk协议
“man7ddp”可以获得具体帮助信息
PF_PACKET
底层包接口
“man7packet”获得具体帮助信息
参数type用于指定套接字的类型。
套接字类型除了前面提到的流套接字、数据报套接字及原始套接字外,还有其他的几种类型,如表13.3所示。
表13.3type参数可取值情况
套接字类型
说明
SOCK_STREAM
提供有序、可靠、双向及基于连接的字节流。
支持带外传输机制
SOCK_DGRAM
支持数据报
SOCK_SEQPACKET
提供有序、可靠、双向基于连接的数据报通信
SOCK_RAW
提供对原始网络协议的访问
SOCK_RDM
提供可靠的数据报层,但是不保证有序性
SOCK_PACKET
该参数已经废除
流套接字(SOCK_STREAM)与管道类似,是一种全双工的比特流。
流套接字在发送或接收数据前必须处于连接状态。
实现流套接字的通信协议保证了传输的数据不会丢失。
参数protocol用于指定套接字使用的通信协议。
正常情况下,对于给定的协议族,只有单一的协议支持特定的套接字类型。
这时,只要将protocol参数设置为0即可。
错误信息:
EACCES:
创建指定类型的套接字失败。
EAFNOSUPPORT:
不支持指定的地址族。
EINVAL:
未知协议或未知的协议族。
EMFILE:
进程文件表溢出。
ENFILE:
达到打开文件的系统限制。
ENOBUFS或ENOMEM:
内存不足。
EPROTONOSUPPORT:
指定的协议类型在该域中不支持。
13.2.3bind函数
bind函数用于将套接字与指定端口相连,其具体信息如表13.4所示。
表13.4bind函数
头文件
函数形式
intbind(intsockfd,conststructsockaddr*my_addr,socklen_taddrlen);
返回值
成功
失败
是否设置errno
0
−1
是
说明:
当调用socket函数创建套接字后,该套接字并没有与本机地址和端口等信息相连,bind函数将完成这些工作。
bind函数中的sockfd参数为调用socket函数后返回的文件描述符。
my_addr参数为指向sockaddr结构体的指针(该结构体中保存有端口和IP地址信息)。
addlen参数为结构体sockaddr的长度。
错误信息:
EACCES:
地址受到保护,用户非超级用户。
EADDRINUSE:
指定的地址已经在使用。
EBADF:
sockfd参数为非法的文件描述符。
EINVAL:
socket已经和地址绑定。
ENOTSOCK:
参数sockfd为文件描述符。
实例演练:
下面的代码来自于Linux系统的man帮助,注意创建套接字和使用bind函数实现socket的文件描述符与地址信息绑定的过程。
#include
#include
#include
#include
#defineMY_SOCK_PATH"/somepath"
int
main(intargc,char*argv[])
{
intsfd;
structsockaddr_unaddr;
//创建通信的套接字
sfd=socket(AF_UNIX,SOCK_STREAM,0);
if(sfd==-1){
perror("socket");
exit(EXIT_FAILURE);
}
//初始化addr变量
memset(&addr,0,sizeof(structsockaddr_un));
addr.sun_family=AF_UNIX;
strncpy(addr.sun_path,MY_SOCK_PATH,
sizeof(addr.sun_path)-1);
//将端口信息与套接字绑定
if(bind(sfd,(structsockaddr*)&addr,sizeof(structsockaddr_un))==-1){
perror("bind");
exit(EXIT_FAILURE);
}
…
}
13.2.4listen函数
服务器必须等待客户的连接请求,listen函数用于实现等待功能,该函数的具体信息如表13.5所示。
表13.5listen函数
头文件
函数形式
intlisten(intsockfd,intbacklog);
返回值
成功
失败
是否设置errno
0
−1
是
说明:
listen函数中,参数sockfd为调用socket函数获得的套接字的文件描述符信息。
backlog参数为提出连接请求后,在服务器接收该连接请求时的等待队列中的连接数。
默认情况,该值为20。
系统调用listen只用于套接字类型为SOCK_STREAM或SOCK_SEQPACKET的场合。
错误信息:
EADDRINUSE:
另一个socket也在监听同一个端口。
EBADF:
参数sockfd为非法的文件描述符。
ENOTSOCK:
参数sockfd不是文件描述符。
EOPNOTSUPP:
套接字类型不支持listen操作。
13.2.5accept函数
处于监听状态的服务器在获得客户机的连接请求后,会将其放置在等待队列中。
当系统空闲时,将接受客户机的连接请求。
接收客户机的连接请求使用accept函数,该函数的具体信息如表13.6所示。
表13.6accept函数
头文件
函数形式
intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);
返回值
成功
失败
是否设置errno
返回新的套接字文件描述符
−1
是
说明:
accept函数用于面向连接类型的套接字类型(SOCK_STREAM和SOCK_SEQPACKET)。
accept函数将从连接请求队列中获得连接信息,创建新的套接字,并返回该套接字的文件描述符。
新创建的套接字用于服务器与客户机的通信,而原来的套接字仍然处于监听状态。
accept函数的sockfd参数为监听的套接字描述符。
addr参数为指向结构体sockaddr的指针。
参数addrlen为addr参数指向的内存空间的长度。
错误信息:
EAGAIN:
套接字处于非阻塞状态,当前没有连接请求。
EBADF:
非法的文件描述符。
ECONNABORTED:
连接中断。
EINTR:
系统调用被信号中断。
EINVAL:
套接字没有处于监听状态,或非法的addrlen参数。
EMFILE:
达到进程打开文件描述符限制。
ENFILE:
达到打开文件数限制。
ENOTSOCK:
文件描述符为文件的文件描述符。
EOPNOTSUPP:
套接字类型不是SOCK_STREAM。
13.2.6connect函数
对于客户机而言,要与服务器进行通信,需要向服务器发出连接请求。
connect函数用于完成这项功能,该函数的具体信息如表13.7所示。
表13.7connect函数
头文件
函数形式
intconnect(intsockfd,conststructsockaddr*serv_addr,socklen_taddrlen);
返回值
成功
失败
是否设置errno
0
−1
是
说明:
connect函数将使用参数sockfd中的套接字连接到参数serv_addr中指定的服务器。
参数addrlen为serv_addr指向的内存空间大小。
如果参数sockfd的类型为SOCK_DGRAM,serv_addr参数为数据报发往的地址,且将只接收该地址的数据报。
如果sockfd的类型为SOCK_STREAM或SOCK_SEQPACKET,调用该函数将连接serv_addr中的服务器地址。
错误信息:
EACCES,EPERM:
用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。
EADDRINUSE:
本地地址处于使用状态。
EAFNOSUPPORT:
参数serv_add中的地址非合法地址。
EAGAIN:
没有足够空闲的本地端口。
EALREADY:
套接字为非阻塞套接字,并且原来的连接请求还未完成。
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.8send函数
头文件
函数形式
ssize_tsend(ints,constvoid*buf,size_tlen,intflags);
返回值
成功
失败
是否设置errno
返回实际发送的数据的字节数
−1
是
说明:
send函数用于将信息发送到指定的套接字文件描述符中。
该函数只能用于已经建立连