图为面向连接的Socket通信的双方执行函数流程。使用TCP协议的通信双方实现数据通信的基本流程如下
建立连接的步骤
1、首先服务器端需要以下工作:
(1)调用socket()函数,建立Socket对象,指定通信协议。
(2)调用bind()函数,将创建的Socket对象与当前主机的某一个IP地址和TCP端口绑定。
(3)调用listen()函数。使Socket对象处于监听状态,并设置监听队列大小。
2、客户端的准备工作:
(1)调用socket函数,建立Socket对象,指定与服务器端相同的通信协议。
(2)应用程序可以调用bind()函数为其绑定IP地址和端口,此工作也可交给TCP/IP完成。
3、建立通信连接
(1)客户端调用connect()函数,向服务器端发出连接请求。
(2)服务端监听到该请求,调用accept()函数接受请求,建立连接,并返回一个新的Socket文件描述符以专门处理该连接。
4、通信双方发送/接收数据
(1)服务器端和客户端分别调用write()或send()函数发送数据read或recv函数接收数据。
(2)通信完成后,通信双方都需要调用close或shutdown来关闭Socket。
熟悉上述流程后,网络编程的重点就在于应用层开发的协议实现上了.
BSD Socket网路编程API
建立套接字,创建socket对象
使用socket函数创建socket,声明如下:
<span style="font-family: Arial; font-size: 14px;">/* Create a new socket of type TYPE in domain DOMAIN, using protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically. R</span><span style="font-family:SimSun;font-size:12px;">eturns a file descriptor for the new socket, or -1 for errors. */ extern int socket (int __domain, int __type, int __protocol) __THROW;</span> |
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
第一个参数用来指明socket对象所使用的地址簇或协议簇,即是此对象使用的协议类型,协议簇的宏定义如下:系统源文件/bit/socket.h
<span style="font-family:SimSun;font-size:12px;">/* Protocol families. */ #define PF_UNSPEC 0 /* Unspecified. */ //未定义 #define PF_LOCAL 1 /* Local to host (pipes and file-domain). */ // 本地通信 #define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */ #define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */ #define PF_INET 2 /* IP protocol family. */ // IPv4 #define PF_AX25 3 /* Amateur Radio AX.25. */ #define PF_IPX 4 /* Novell Internet Protocol. */ #define PF_INET6 10 /* IP version 6. */ // IPv6</span> |
地址簇是协议簇的重定义:
<span style="font-family:SimSun;font-size:12px;">/* Address families. */ #define AF_UNSPEC PF_UNSPEC #define AF_LOCAL PF_LOCAL #define AF_UNIX PF_UNIX #define AF_FILE PF_FILE #define AF_INET PF_INET #define AF_AX25 PF_AX25 #define AF_IPX PF_IPX</span> |
第二个参数是socket的类型,系统的定义如下:
<span style="font-family:SimSun;font-size:12px;"> 41: /* Types of sockets. */ 42: enum __socket_type 43: { 44: SOCK_STREAM = 1, /* Sequenced, reliable, connection-based 45: byte streams. */ 46: #define SOCK_STREAM SOCK_STREAM 47: SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams 48: of fixed maximum length. */ 49: #define SOCK_DGRAM SOCK_DGRAM 50: SOCK_RAW = 3, /* Raw protocol interface. */ 51: #define SOCK_RAW SOCK_RAW 52: SOCK_RDM = 4, /* Reliably-delivered messages. */ 53: #define SOCK_RDM SOCK_RDM 54: SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based, 55: datagrams of fixed maximum length. */ 56: #define SOCK_SEQPACKET SOCK_SEQPACKET 57: SOCK_PACKET = 10 /* Linux specific way of getting packets 58: at the dev level. For writing rarp and 59: other similar things on the user level. */ 60: #define SOCK_PACKET SOCK_PACKET 61: };</span> |
如上面的代码所示,有六种socket类型,最基本的类型位面向连接的数据流方式和面向无连接的数据报方式.第三个参数表示使用哪一个协议,设置为0则系统自动选择.
Linux在利用socket()系统调用建立新的套接字时,需要传递套接字的地址族标识符、套接字类型以及协议,其函数定义于net/socket.c中:
<span style="font-family:SimSun;font-size:12px;">asmlinkagelong sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; retval = sock_create(family, type, protocol,&sock); if (retval < 0) goto out; retval = sock_map_fd(sock); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }</span> |
实际上,套接字对于用户程序而言就是特殊的已打开的文件。内核中为套接字定义了一种特殊的文件类型,形成一种特殊的文件系统sockfs,其定义于net/socket.c:
<span style="font-family:SimSun;font-size:12px;"> static struct vfsmount *sock_mnt; static DECLARE_FSTYPE(sock_fs_type, "sockfs",sockfs_read_super, FS_NOMOUNT);</span> |
在系统初始化时,要通过kern_mount()安装这个文件系统。安装时有个作为连接件的vfsmount数据结构,这个结构的地址就保存在一个全局的指针sock_mnt中。所谓创建一个套接字,就是在sockfs文件系统中创建一个特殊文件,或者说一个节点,并建立起为实现套接字功能所需的一整套数据结构。所以,函数sock_create()首先是建立一个socket数据结构,然后将其“映射”到一个已打开的文件中,进行socket结构和sock结构的分配和初始化。
新创建的 BSD socket 数据结构包含有指向地址族专有的套接字例程的指针,这一指针实际就是proto_ops
数据结构的地址。
BSD 套接字的套接字类型设置为所请求的 SOCK_STREAM 或 SOCK_DGRAM
等。然后,内核利用 proto_ops 数据结构中的信息调用地址族专有的创建例程。
之后,内核从当前进程的 fd 向量中分配空闲的文件描述符,该描述符指向的
file 数据结构被初始化。初始化过程包括将文件操作集指针指向由 BSD 套接字接口支持的 BSD 文件操作集。所有随后的套接字(文件)操作都将定向到该套接字接口,而套接字接口则会进一步调用地址族的操作例程,从而将操作传递到底层地址族,如图12.10所示。
实际上,socket结构与sock结构是同一事物的两个方面。如果说socket结构是面向进程和系统调用界面的,那么sock结构就是面向底层驱动程序的。可是,为什么不把这两个数据结构合并成一个呢?
我们说套接字是一种特殊的文件系统,因此,inode结构内部的union的一个成分就用作socket结构,其定义如下:
<span style="font-family:SimSun;font-size:12px;">struct inode { … union { … struct socket socket_i; } }</span> |
由于套接字操作的特殊性,这个结构中需要大量的结构成分。可是,如果把这些结构成分全都放在socket结构中,则inode结构中的这个union就会变得很大,从而inode结构也会变得很大,而对于其他文件系统,这个union成分并不需要那么庞大。因此,就把套接字所需的这些结构成分拆成两部分,把与文件系统关系比较密切的那一部分放在socket结构中,把与通信关系比较密切的那一部分则单独组成一个数据结构,即sock结构。由于这两部分数据在逻辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系。
绑定本地IP地址和端口
为了监听传入的 Internet 连接请求,每个服务器都需要建立一个 INET
BSD 套接字,并且将自己的地址绑定到该套接字。绑定操作主要在 INET 套接字层中进行,还需要底层 TCP
层和 IP 层的某些支持。将地址绑定到某个套接字上之后,该套接字就不能用来进行任何其他的通讯,因此,该
socket数据结构的状态必须为 TCP_CLOSE。传递到绑定操作的 sockaddr 数据结构中包含要绑定的
IP地址,以及一个可选的端口地址。通常而言,要绑定的地址应该是赋予某个网络设备的 IP 地址,而该网络设备应该支持
INET 地址族,并且该设备是可用的。利用 ifconfig 命令可查看当前活动的网络接口。被绑定的 IP
地址保存在 sock 数据结构的rcv_saddr 和 saddr 域中,这两个域分别用于哈希查找和发送用的
IP 地址。端口地址是可选的,如果没有指定,底层的支持网络会选择一个空闲的端口。使用bind函数.
/* Give the socket FD the local address ADDR (which is LEN bytes long). */ extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len) __THROW; |
此函数将指定的socket与对应网络地址绑定,成功返回0,失败返回-1
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲过,struct
sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:
<span style="font-family:SimSun;font-size:12px;">bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT);</span> |
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000。
当底层网络设备接受到数据包时,它必须将数据包传递到正确的 INET 和
BSD 套接字以便进行处理,因此,TCP维护多个哈希表,用来查找传入 IP 消息的地址,并将它们定向到正确的socket/sock
对。TCP 并不在绑定过程中将绑定的 sock 数据结构添加到哈希表中,在这一过程中,它仅仅判断所请求的端口号当前是否正在使用。在监听操作中,该
sock 结构才被添加到 TCP 的哈希表中。
监听网络
绑定了ip信息和端口信息的socket对象还不能进行tcp通讯,因为当前还没有能力监听网络请求,因此,对于面向连接的用户来说,服务器端通常需要调用listen函数使该socket对象监听网络,函数声明如下:
/* Prepare to accept connections on socket FD. N connection requests will be queued before further requests are refused. Returns 0 on success, -1 for errors. */ extern int listen (int __fd, int __n) __THROW; |
listen()用来等待参数s 的socket连线。参数backlog指定同时能处理的最大连接要求,如果连接数目达此上限则client端将收到ECONNREFUSED的错误。Listen()并未开始接收连线,只是设置socket为listen模式,真正接收client端连线的是accept()。通常listen()会在socket(),bind()之后调用,接着才调用accept()。
返回值
成功则返回0,失败返回-1,错误原因存于errno
附加说明
listen()只适用SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socket为AF_INET则参数backlog
最大值可设至128。
错误代码
EBADF 参数sockfd非合法socket处理代码
EACCESS 权限不足
EOPNOTSUPP 指定的socket并未支援listen模式。
客户端发起连接
如果服务器已经监听网络,且客户端创建了socket对象,此时,客户端可以使用connect函数与服务器建立连接.声明如下
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long). For connectionless socket types, just set the default address to send to and the only address from which to accept transmissions. Return 0 on success, -1 for errors. This function is a cancellation point and therefore not marked with __THROW. */ extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len); |
函数说明
connect()用来将参数sockfd 的socket 连至参数serv_addr
指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。
返回值
成功则返回0,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数sockfd 非合法socket处理代码
EFAULT 参数serv_addr指针指向无法存取的内存空间
ENOTSOCK 参数sockfd为一文件描述词,非socket。
EISCONN 参数sockfd的socket已是连线状态
ECONNREFUSED 连线要求被server端拒绝。
ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。
ENETUNREACH 无法传送数据包至指定的主机。
EAFNOSUPPORT sockaddr结构的sa_family不正确。
EALREADY socket为不可阻断且先前的连线操作还未完成
服务器接受连接
如果服务器端监听到客户端的连接请求,则需要调用accept函数接收请求.如果没有监听到客户端的连接请求,此函数处于阻塞状态.声明如下:
/* Await a connection on socket FD. When a connection arrives, open a new socket to communicate with it, set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting peer and *ADDR_LEN to the address's actual length, and return the new socket's descriptor, or -1 for errors. This function is a cancellation point and therefore not marked with __THROW. */ extern int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len); #ifdef __USE_GNU /* Similar to 'accept' but takes an additional parameter to specify flags. This function is a cancellation point and therefore not marked with __THROW. */ extern int accept4 (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len, int __flags); |
函数说明
accept()用来接受参数s的socket连线。参数s的socket必需先经bind()、listen()函数处理过,当有连线进来时accept()会返回一个新的socket处理代码,往后的数据传送与读取就是经由新的socket处理,而原来参数s的socket能继续使用accept()来接受新的连线要求。连线成功时,参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构长度。关于结构sockaddr的定义请参考bind()。
返回值
成功则返回新的socket处理代码,失败返回-1,错误原因存于errno中。
错误代码
EBADF 参数s 非合法socket处理代码。
EFAULT 参数addr指针指向无法存取的内存空间。
ENOTSOCK 参数s为一文件描述词,非socket。
EOPNOTSUPP 指定的socket并非SOCK_STREAM。
EPERM 防火墙拒绝此连线。
ENOBUFS 系统的缓冲内存不足。
ENOMEM 核心内存不足。
读写socket对象
socket对象是一类特殊的文件,因此可以使用Linux系统的I/O系统调用read函数来读socket对象数据,write函数向socket对象写入数据.
函数声明与使用和之前的一样.
TCP发送接收数据
send和recv函数专门实现面向连接的socket对象读写操作.
/* Send N bytes of BUF to socket FD. Returns the number sent or -1. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags); /* Read N bytes into BUF from socket FD. Returns the number read or -1 for errors. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags); |
关于send函数
第一个参数是发送的目标socket对象,第二个参数是欲发送的数据位置,第三个参数是数据的大小.第四个参数操作flags,支持的值位0或者MSG_OOB(发送带外数据)等.使用send函数时将flags置为0与使用write函数完全相同.
执行成功返回数据大小,失败返回-1
关于recv函数
各个参数类似于send函数各个参数定义,其将从fd所指的socket中读取n字节数据到buff中.执行成功则返回数据大小,失败返回-1
两个函数的flags用来说明处理数据的方式,常见的声明如下socket.h中定义:
/* Bits in the FLAGS argument to `send', `recv', et al. */ enum { MSG_OOB = 0x01, /* Process out-of-band data. */ // 带外数据 #define MSG_OOB MSG_OOB MSG_PEEK = 0x02, /* Peek at incoming messages. */ // 查看外来信息,相同不丢失查看到的数据 #define MSG_PEEK MSG_PEEK MSG_DONTROUTE = 0x04, /* Don't use local routing. */ // 本地不路由 #define MSG_DONTROUTE MSG_DONTROUTE #ifdef __USE_GNU /* DECnet uses a different name. */ MSG_TRYHARD = MSG_DONTROUTE, # define MSG_TRYHARD MSG_DONTROUTE #endif MSG_CTRUNC = 0x08, /* Control data lost before delivery. */ #define MSG_CTRUNC MSG_CTRUNC MSG_PROXY = 0x10, /* Supply or ask second address. */ #define MSG_PROXY MSG_PROXY MSG_TRUNC = 0x20, #define MSG_TRUNC MSG_TRUNC MSG_DONTWAIT = 0x40, /* Nonblocking IO. */ // 不阻塞 #define MSG_DONTWAIT MSG_DONTWAIT MSG_EOR = 0x80, /* End of record. */ #define MSG_EOR MSG_EOR MSG_WAITALL = 0x100, /* Wait for a full request. */ #define MSG_WAITALL MSG_WAITALL // 等待所有数据 MSG_FIN = 0x200, #define MSG_FIN MSG_FIN MSG_SYN = 0x400, #define MSG_SYN MSG_SYN MSG_CONFIRM = 0x800, /* Confirm path validity. */ #define MSG_CONFIRM MSG_CONFIRM MSG_RST = 0x1000, #define MSG_RST MSG_RST MSG_ERRQUEUE = 0x2000, /* Fetch message from error queue. */ #define MSG_ERRQUEUE MSG_ERRQUEUE MSG_NOSIGNAL = 0x4000, /* Do not generate SIGPIPE. */ #define MSG_NOSIGNAL MSG_NOSIGNAL MSG_MORE = 0x8000, /* Sender will send more. */ #define MSG_MORE MSG_MORE MSG_WAITFORONE = 0x10000, /* Wait for at least one packet to return.*/ #define MSG_WAITFORONE MSG_WAITFORONE MSG_FASTOPEN = 0x20000000, /* Send data in TCP SYN. */ #define MSG_FASTOPEN MSG_FASTOPEN MSG_CMSG_CLOEXEC = 0x40000000 /* Set close_on_exit for file descriptor received through SCM_RIGHTS. */ #define MSG_CMSG_CLOEXEC MSG_CMSG_CLOEXEC }; |
关闭socket对象
使用I/O相同调用close函数.
另一种是调用shutdown函数,
/* Shut down all or part of the connection open on socket FD. HOW determines what to shut down: SHUT_RD = No more receptions; SHUT_WR = No more transmissions; SHUT_RDWR = No more receptions or transmissions. Returns 0 on success, -1 for errors. */ extern int shutdown (int __fd, int __how) __THROW; |
TCP连接是双向的(可读可写),当使用close函数时,会把读写均关闭,有时候希望只关闭一个方向,这时要用shutdown函数,相同提供了下面三种关闭方式.
howto = 0 关闭读通道,可以继续向socket描述符中写
howto = 1 关闭写通道.此时只可以读
howto = 2 关闭读写
获取socket本地以及对端信息
/* Put the local address of FD into *ADDR and its length in *LEN. */ extern int getsockname (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __len) __THROW; /* Put the address of the peer connected to socket FD into *ADDR (which is *LEN bytes long), and its actual length into *LEN. */ extern int getpeername (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __len) __THROW; |
|