Linux Socket函数库是从Berkeley大学开发的BSD UNIX系统中移植过来的。BSD Socket接口是众多Unix系统中被广泛支持的TCP/IP通信接口,Linux下的Socket程序设计,除了微小的差别之外,也适用于大多数其它Unix系统。
Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。
Socket的使用和文件操作比较类似。如同文件的读、写、打开、关闭等操作一样,TCP/IP网络通信同样也有这些操作,不过它使用的接口不是文件描述符或者FILE*,而是一个称做Socket的描述符。类似于文件操作,对于Socket,也通过读、写、打开、关闭操作来进行网络数据传送。同时,还有一些辅助的函数,如域名/IP地址查询、Socket功能设置等。
套接字有三种类型:流式套接字、数据报套接字及原始套接字。
流式套接字定义了一种可靠的面向连接的服务,实现了无差错无重复的顺序数据传输。数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠、无差错。原始套接字允许对底层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。
套接字工作过程如下:服务器首先启动,通过调用socket()建立一个套接字,然后调用bind()将该套接字和本地网络地址联系在一起,再调用listen()使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用accept()来接收连接。客户在建立套接字后就可以调用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。最后,待数据传送结束后,双方调用close()关闭套接字。
Socket函数库:
1. socket(int domain, int type,int protocol):此函数分配一个Socket句柄,用于指定特定网络下,使用特定的协议和数据传送方式进行通信。Socket句柄分配以后,如果要开始TCP通信,还需要建立连接。根据需要,可以主动地建立连接(通过connect())和被动地等待对方建立连接(通过listen()),在连接建立后才能使用读写操作通过网络连接进行数据交换。
domain参数选择通信中使用的协议族,也就是网络的类型,可以是以下之一:(1)、AF_UNIX:UNIX内部协议;(2)、AF_INET:ARPA Internet协议,也就是TCP/IP协议族;(3)、AF_ISO:ISO协议;(4)、AF_NS:Xerox Network Systems协议;(5)、AF_IMPLINK:IMP “host at IMP” link layer。
type参数为数据传送的方式,可以是以下之一:(1)、SOCK_STREAM:保证顺序的、可靠传送的双向字节数据流,最为常用,也是TCP连接所使用的方式;(2)、SOCK_DGRAM:无连接的、不保证可靠的、固定长度(通常很小)的消息传送;(3)、SOCK_SEQPACKET:顺序的、可靠的双向固定长度的数据报传送,只用于AF_NS类型的网络中;(4)、SOCK_RAW:原始的数据传送,适用于系统内部专用的网络协议和接口,和SOCK_RDM一样,只能由超级用户使用;(5)、SOCK_RDM:可靠的数据报传送,未实现。
protocol参数指定通信中使用的协议。在给定Socket的协议族和传送类型之后,一般情况下所使用的协议也就固定下来,此时protocol参数可使用缺省值”0”。但如果还有多个协议供选择,则必须使用protocol参数来标识。
socket()函数,正常执行时,返回Socket描述符;否则,返回-1,错误状态在全局变量errno中。
2. close(int fd):Socket和文件描述符的关闭操作都是使用这个函数。fd参数为Socket描述符。返回值,正常是返回0,-1表示出错。
3. bind(int sockfd, structsockaddr *my_addr, int addrlen):此函数给已经打开的Socket指定本地地址。这个函数的使用有以下两种情况:(1)、如果此Socket是面向连接的,而且此Socket在连接建立过程中处于被动的地位,即,乙方程序使用listen函数等待对方建立连接,对方用connect函数来向此Socket建立连接,这种情况下,必须用bind给此Socket设定本地地址。在乙方使用listen函数时,除指定Socket描述符之外,该Socket必须已经用bind函数设定好了本地地址(包括IP地址和端口号),这样,系统在收到建立连接的网络请求时,才能根据请求的目的地址,识别是通向哪个Socket的连接,从而乙方才能用此Socket接收到发给此Socket地址的数据包。不指定Socket的本地地址,就无法将此Socket用于连接建立和数据接收。(2)、如果此Socket用于无连接的情形,同样也要求给该Socket设定本地地址,这样,以后系统从网络中接收到数据后,才知道该送给哪个Socket及其相对应的进程。
sockfd参数:用于指定Socket描述符。
addrlen参数:my_addr结构的长度。
my_addr参数:用于侦听连接请求的本地地址。struct sockaddr是一个通用性的结构,不仅包含TCP/IP协议的情况,同时也是为了适合于其它网络,如AF_NS。由于它的这种通用性,它只是定义了一个一般意义上的存储空间。
bind函数返回值:正常时返回0,否则返回-1,同时errno是系统错误码。
4. listen(int s, int backlog):准备接收连接请求。在用bind()给一个Socket设定本地地址后,就可以将这个Socket用于接收连接请求,即listen()。调用listen()之后,系统将给此Socket配备一个连接请求的队列,暂存系统接收到的、申请向此Socket建立连接的请求,等待用户程序用accept()正式接收该请求。队列长度,就由backlog参数指定。如果短时间内向乙方建立连接的请求过多,乙方来不及处理,那么排在backlog之后的请求将被系统拒绝。因此,backlog参数实际上规定了乙方程序能够容许的连接建立处理速度。至于乙方程序使用此Socket(及其指定的本地地址)实际建立连接的个数,由乙方程序调用accept()的次数来决定。
listen函数返回值:正常时返回0,否则返回-1.
5. accept(int s, struct sockaddr*addr, int *addrlen):接受指定Socket上的连接请求。在调用listen()之后,系统就在Socket的连接请求暂存队列里存放每一个向该Socket(及其本地地址)建立的连接请求。accept()函数的作用就是,从该暂存队列中取出一个连接请求,用该Socket的数据,创建一个新的Socket:Socket_New,并为它分配一个文件描述符。Socket_New即标识了此次建立的连接,可被乙方用来向连接的另一方发生和接收数据(write/read, send/recv)。同时,原Socket仍然保持打开状态不变,继续用于等待网络连接请求。
如果该Socket的暂存队列中没有待处理的连接请求,根据Socket的特征选项(是否non_blocking),blocking即阻塞,accept()函数将选择两种方式:如果该Socket不是non_blocking型的,accept()将一直等待,直到收到一个连接请求后才返回;如果该Socket是non_blocking型的,那么accept()将立即返回,但如果没有连接请求,只返回错误信息,不创建新的Socket_New。accept()返回后,如果创建了新的Socket_New来标识新建立的连接,那么参数addr指定的结构里面将会有对方的地址信息,addrlen是地址信息的长度。
s参数:Socket描述符。
addr参数:accept()接受连接后,在addr指向的结构中存放对方的地址信息。如果是AF_INET Socket,该地址信息就是地方的IP地址和端口号。
addrlen参数:在调用accept()之前,*addrlen必须被设置为addr数据结构的合法长度。在accept()返回之后,*addrlen中是对方地址信息的长度。
accept函数返回值:如果正常创建了一个新的连接,那么返回非负的整数,即新连接的Socket描述符(注意,用于等待连接请求的原Socket保持打开状态不变,可用于接收新的连接请求),否则,返回-1.
bind、listen、connect等函数,都是用于被动地等待对方建立连接时需要使用的,而connect函数,则是主动地向对方建立连接时使用的。connect()使用一个事先打开的Socket,和目的方(即通信对方,或称服务器一方)地址信息,向对方发出连接建立请求。
6. connect(int sockfd, structsockaddr *serv_addr, int addrlen):客户端发送服务请求。
sockfd参数:socket函数返回的socket描述符。
serv_addr:存储远程计算机的IP地址和端口信息的结构。
addrlen:是结构体sockaddr_in的长度。
返回值:成功返回0,否则返回-1.
7. send(int s, const void *msg,int len, unsigned int flags)/sendto(int s, const void *msg, int len, unsignedint flags, const struct sockaddr *to, int tolen),recv(int s, void*buf, int len, unsigned iint flags)/recvfrom(int s, void *buf, int len,unsigned int flags, struct sockaddr *from, int *fromlen):用Socket发送和接收数据。在连接建立完成后,通信双方就可以使用以上这些函数来进行数据的发送和接收操作。其中,send和recv用于连接建立以后的发送和接收。sendto和recvfrom用于非连接的协议。对于非non_blocking型的Socket,send将等待数据发送完后才返回;对于non_blocking型的Socket,send将立即返回,用户程序需要用select()函数决定网络发送是否结束。类似地,对于非non_blocking型的Socket,若系统没有收到任何数据,recv将等待接收数据到达后才返回;对于non_blocking型的Socket,recv将立即返回,并返回错误信息或接收到的数据字节数。sendto和recvfrom因为是非连接型的发送和接收,必须在参数中给出目的地址或者存放源地址的空间。
s参数:Socket描述符。
msg,buf参数:存放接收或者发送数据的存储空间。
len参数:接收或者发送数据的字节数。
to,from参数:sendto和recvfrom所使用的,目的方地址和存放源地址的空间。
tolen,fromlen参数:目的地址和源地址空间大小。
flag参数:通常设为0.
返回值:send/sendto返回实际发送的数据字节数,或者-1,表示出错。recv/recvfrom返回实际接收到的数据字节数,或者-1,表示出错。
8. read(int fd, void *buf, size_tcount)/write(int fd, const void *buf, size_t count):用系统文件操作进行Socket通信。在连接建立完成后,对于连接建立过程中被动的一方,在accept()正常返回后,它返回一个新的Socket,并且为该Socket分配了一个文件描述符;对于连接请求发起方,connect()正常返回后,相应的Socket中也包含有已分配的文件描述符。因此,可以使用标准的Unix文件读写函数read()/write()来进行Socket通信。要注意的是,由于网络数据和磁盘文件不一样,不是已经准备好的,因此,每次读写操作不一定能传送完指定长度的数据,需要由程序反复进行剩余部分的传送。另外,文件描述符是较底层的文件操作函数,不同于C语言中常用的FILE*。FILE*是使用fread/fwrite函数来进行读写操作的。
fd参数:文件或者Socket描述符。
buf参数:数据缓冲区。
count参数:数据字节数。
函数返回值:正常时,返回所读写的字节数(注意,可能小于count参数指定的数目);否则,返回-1.
9. getsockopt(int s, int level,int optname, void *optval, int *optlen)/setsockopt(int s, int level, intoptname, const void *optval, int optlen):获取、设置Socket特征选项。
常用的几个转换函数:(1)、inet_addr:将IP地址从点数格式转换成无符号长整型,它返回的地址是网络字节格式;(2)、inet_ntoa:将一个in_addr结构体输出成点数格式;(3)、htonl:将32位的主机字节顺序转化为32位的网络字节顺序;(4)、htons:将16位的主机字节顺序转化为16位的网络字节顺序;(5)、ntohs:将一个无符号短整形数从网络字节顺序转化为主机字节顺序;(6)、ntohl:将一个无符号长整形数从网络主机顺序转化为主机字节顺序。
以下是测试用例:
1. client.cpp:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <iostream> #define PORT 7000 int main() { struct sockaddr_in server; int s, ns; int pktlen, buflen; char buf1[256], buf2[256]; s = socket(AF_INET, SOCK_STREAM, 0); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htons(INADDR_ANY); if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) { perror("connect()"); return -1; } for (;;) { printf("Enter a line: "); std::cin>>buf1; buflen = strlen(buf1); if (buflen == 1) break; send(s, buf1, buflen+1, 0); recv(s, buf2, sizeof(buf2), 0); printf("Received line: %s\n", buf2); } close(s); return 0; }
2. server.cpp:
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <ctype.h> #define PORT 7000 int main() { struct sockaddr_in client, server; int s, ns, pktlen; char buf[256]; s = socket(AF_INET, SOCK_STREAM, 0); memset((char*)&server, sizeof(server), 0); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = htons(INADDR_ANY); bind(s, (struct sockaddr*)&server, sizeof(server)); listen(s, 1); socklen_t namelen = sizeof(client); ns = accept(s, (struct sockaddr*)&client, &namelen); for (;;) { pktlen = recv(ns, buf, sizeof(buf), 0); if (pktlen == 0) break; printf("Received line:%s\n", buf); for (int i = 0; i < strlen(buf); i++) buf[i] = toupper(buf[i]); send(ns, buf, pktlen, 0); } close(ns); close(s); return 0; }
执行说明:(1)、打开终端,分别执行:$ g++ -o server server.cpp , $ g++ -o client client.cpp ; (2)、打开终端,先执行服务器端程序:$ ./server ;再打开另一终端,执行客户端程序:$ ./client ;(3)、程序功能:服务器端接收从客户端来的数据,并将其接收的数据小写字母改为大写再发送给客户端,在客户端显示接收后的结果数据。当输入一个字符长度时退出。
以上部分内容整理自网络。