上一节介绍了套接字有3种类型,其中流套接字可以实现可靠的数据传输。本节将介绍如何使用流套接字实现网络中主机间的通信。
13.2.1 工作流程
使用流套接字实现网络中不同主机间的通信属于典型的服务器/客户机模型,即客户端向服务器发送服务请求,服务器根据该请求提供相应的服务。
图13.3所示为简单的通信示意图。为了实现服务器与客户机间的通信,服务器和客户机都必须创建套接字。服务器在创建套接字后,需要指定监听的端口来等待客户机,因此,还有绑定端口号的操作。之后,服务器将处于监听状态,等待客户机来连接指定端口。当接收到客户机的连接请求后,服务器调用accept函数来建立与客户机间的通信。在成功建立通信后,就可以通过read函数或write函数进行通信。
客户端处的流程与服务端相比,简单的一些。客户端在创建套接字后,调用connect函数去连接服务器指定的端口。在服务器接收连接后,客户机与服务器之间就可以通过write函数和read函数实现数据通信了。

图13.3 流套接字通信示意图
下面将对图13.3中用于实现流套接字通信的函数进行详细介绍。
13.2.2 socket函数
socket函数的具体信息如表13.1所示。
表13.1 socket函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
int socket(int domain, int type, int protocol); |
||
|
返回值 |
成功 |
失败 |
是否设置errno |
|
创建的socket的文件描述符 |
−1 |
是 |
|
说明:
socket函数用于创建通信的套接字,并返回该套接字的文件描述符。参数domain指定了通信域,该参数用于选择通信协议族,其取值情况如表13.2所示。
表13.2 domain取值情况表
|
名 称 |
含 义 |
备 注 |
|
PF_UNIX, PF_LOCAL |
本地通信 |
“man 7 UNIX”可以获得具体帮助信息 |
|
PF_INET |
IPv4协议 |
“man 6 ip”可以获得具体帮助信息 |
|
PF_INET6 |
IPv6协议 |
− |
|
PF_IPX |
Novell公司的IPX协议 |
− |
|
PF_NETLINK |
与内核间的接口 |
“man 7 netlink”可以获得具体帮助信息 |
|
PF_X25 |
ITU-T X.25 / ISO-8208 |
“man 7 x25”可以获得具体帮助信息 |
|
PF_AX25 |
无线AX.25协议 |
− |
|
PF_ATMPVC |
访问原始ATM的PVC |
− |
|
PF_APPLETALK |
苹果公司的Appletalk协议 |
“man 7 ddp”可以获得具体帮助信息 |
|
PF_PACKET |
底层包接口 |
“man 7 packet”获得具体帮助信息 |
参数type用于指定套接字的类型。套接字类型除了前面提到的流套接字、数据报套接字及原始套接字外,还有其他的几种类型,如表13.3所示。
表13.3 type参数可取值情况
|
套接字类型 |
说 明 |
|
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.3 bind函数
bind函数用于将套接字与指定端口相连,其具体信息如表13.4所示。
表13.4 bind函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen); |
||
|
返回值 |
成功 |
失败 |
是否设置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 <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdlio.h>
#define MY_SOCK_PATH "/somepath"
int
main(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));
addr.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函数
|
头文件 |
<sys/socket.h> |
||
|
函数形式 |
int listen(int sockfd, int backlog); |
||
|
返回值 |
成功 |
失败 |
是否设置errno |
|
0 |
−1 |
是 |
|
说明:
listen函数中,参数sockfd为调用socket函数获得的套接字的文件描述符信息。backlog参数为提出连接请求后,在服务器接收该连接请求时的等待队列中的连接数。默认情况,该值为20。
系统调用listen只用于套接字类型为SOCK_STREAM或SOCK_SEQPACKET的场合。
错误信息:
EADDRINUSE:另一个socket也在监听同一个端口。
EBADF:参数sockfd为非法的文件描述符。
ENOTSOCK:参数sockfd不是文件描述符。
EOPNOTSUPP:套接字类型不支持listen操作。
13.2.5 accept函数
处于监听状态的服务器在获得客户机的连接请求后,会将其放置在等待队列中。当系统空闲时,将接受客户机的连接请求。接收客户机的连接请求使用accept函数,该函数的具体信息如表13.6所示。
表13.6 accept函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
int accept(int sockfd, struct sockaddr *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.6 connect函数
对于客户机而言,要与服务器进行通信,需要向服务器发出连接请求。connect函数用于完成这项功能,该函数的具体信息如表13.7所示。
表13.7 connect函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); |
||
|
返回值 |
成功 |
失败 |
是否设置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.8 send函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
ssize_t send(int s, const void *buf, size_t len, int flags); |
||
|
返回值 |
成功 |
失败 |
是否设置errno |
|
返回实际发送的数据的字节数 |
−1 |
是 |
|
说明:
send函数用于将信息发送到指定的套接字文件描述符中。该函数只能用于已经建立连接的socket通信中,即只用于面向连接的通信中。参数s为要发送数据的套接字文件描述符。buf参数为指向要发送数据的指针。len为要发送数据的长度。flag参数可以包含如下的参数。
l MSG_CONFIRM(Linux 2.3以上的内核版本支持):通知数据链路层发生了转发,且得到了通信另一段的回应。如果链路层没有得到回应,将使用ARP或其他协议来探测网络上的主机。该参数只用于SOCK_DGRAM和SOCK_RAW类型的套接字。
l MSG_DONTROUTE:不通过网关发送数据,只将数据发送到同一子网中的计算机。该参数通常用于诊断或路由程序中,只用于路由的协议族中,包套接字不能使用该参数。
l MSG_DONTWAIT:使用非阻塞操作。如果操作将阻塞,并返回EAGAIN错误。
l MSG_EOR:结束记录(当套接字类型是SOCK_SEQPACKET时使用)。
l MSG_MORE(Linux 2.4.4以上内核版本支持):调用者有更多的数据要发送。
l MSG_OOB:通过套接字发送带外数据(套接字需要支持这一行为,例如,使用SOCK_STREAM类型的套接字)。
write函数与send函数在flag为0时的功能相同。
错误信息:
下面列出send函数常见的错误信息。
EBADF:非法的文件描述。
ECONNRESET:连接重置。
EDESTADDRREQ:在套接字操作中没有指定目标地址。
EFAULT:参数指向了非法的地址空间。
EINTR:数据发送前,捕获到信号。
EINVAL:非法参数。
ENOTSOCK:参数非套接字的文件描述符。
ENOMEM:内存不足。
recv函数可以实现从指定套接字中读取发送来的消息,该函数的具体信息如表13.9所示。
表13.9 recv函数
|
头文件 |
<sys/types.h> <sys/socket.h> |
||
|
函数形式 |
ssize_t recv(int s, void *buf, size_t len, int flags); |
||
|
返回值 |
成功 |
失败 |
是否设置errno |
|
0 |
−1 |
是 |
|
说明:
recv函数用于从指定套接字中获取发送的消息。与send函数一样,该函数只能用于已经建立连接的socket通信中,即只用于面向连接的通信中。参数s为要读取信息的套接字文件描述符。buf参数为指向要保存数据缓冲区的指针。而len为该缓存的最大长度。
参数flags可以包含如下的标志。
l MSG_DONTWAIT:使用非阻塞操作。如果操作将阻塞,将返回EAGAIN错误。
l MSG_OOB:通过套接字发送带外数据(套接字需要支持这一行为,例如使用SOCK_STREAM类型的套接字)。
l MSG_PEEK:该标志表示从接收队列的开始处查看数据,而不从缓冲区中删除数据。
l MSG_TRUNC:返回包的真实长度,即使该长度超出了传递的缓存长度。该标志只用于流套接字。
l MSG_WAITALL:该标志将使得操作处于阻塞状态,直到获得全部数据
当flags参数为0时,recv函数等同与read函数的功能。
错误信息:
EAGAIN:在接收到数据前,接收操作处于阻塞或直至超时。
EBADF:非法的文件描述符。
ECONNABORTED:远程主机拒绝网络连接。
EFAULT:指向接收数据的缓冲区指针指向了非法地址空间。
EINTR:系统调用被信号中断。
EINVAL:非法参数。
ENOTCONN:套接字使用了面向连接的协议,但是并没有建立连接。
ENOTSOCK:文件描述符为文件的文件描述符。
13.2.8 关闭套接字
在完成通信后,可以使用close函数或shutdown函数来关闭套接字。close函数的调用形式为:
close(sockfd);






