套接字(Socket)通信原理
套接字通信允许互联的位于不同计算机上的进程之间实现通信功能。
套接字的属性
套接字的特性由3个属性确定,它们分别是:域、类型和协议。
套接字的域
它指定套接字通信中使用的网络介质,最常见的套接字域是AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
另一个域AF_UNIX表示UNIX文件系统,就是文件输入/输出,它的地址就是文件名。
套接字类型
因特网提供了两种通信机制:流(stream)和数据报(datagram),因而套接字的类型也就分为流套接字和数据报套接字。我们主要看流套接字。
流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。
流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
与流套接字相对的是由类型SOCK_DGRAM指定的数据报套接字,它不需要建立连接和维持一个连接,它们在AF_INET中通常是通过UDP/IP实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并不需要总是要建立和维持一个连接。
套接字协议
只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。通常只需要使用默认值。
套接字地址
每个套接字都有其自己的地址格式,对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件
struct sockaddr_un{
sa_family_t sun_family; //AF_UNIX,它是一个短整型
char sum_path[]; //路径名
};
对于AF_INET域套接字来说,它的地址结构由sockaddr_in来描述,它至少包括以下几个成员:
struct sockaddr_in{
short int sin_family; //AN_INET
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
}
而in_addr被定义为:
struct in_addr{
unsigned long int s_addr;
}
基于流套接字的客户/服务器的工作流程
使用socket进行进程通信的进程采用的客户/服务器系统是如何工作的呢?
服务器端
首先,服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
接下来,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
然后,系统调用listen来创建一个队列,并将其用于存放来自客户的进入连接。
最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接。
客户端
基于socket的客户端比服务器端简单。同样,客户应用程序首先调用socket来创建一个未命名的套接字,然后讲服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信。
流式socket的接口及作用
socket的接口函数声明在头文件
创建套接字——socket系统调用
该函数来创建一个套接字,并返回一个描述符,该描述符可以用来访问该套接字,其原型如下:
int socket(int domain, int type, int protocol)
函数中的三个参数分别对应前面所说的三个套接字属性。protocol参数设置为0表示使用默认协议。
命名(绑定)套接字——bind系统调用
该函数把通过socket调用创建的套接字命名,从而让它可以被其他进程使用。对于AF_UNIX,调用该函数后套接字就会关联到一个文件系统路径名,对于AF_INF,则会关联到一个IP端口号。函数原型如下:
int bind(int socket, const struct sockaddr *address, size_t address_len)
成功时返回0,失败时返回-1;
创建套接字队列(监听)——listen系统调用
该函数用来创建一个队列来保存未处理的请求。成功时返回0,失败时返回-1。其原型如下:
int listen(int socket, int backlog)
backlog用于指定队列的长度,等待处理的进入连续的个数最多不能超过这个数字,否则往后的连接将被拒绝,导致客户的连接请求失败。调用后,程序一直会监听这个IP端口,如果有连接请求,就把它加入到这个队列中。
接受连接——accept系统调用
该系统调用用来等待客户建立对该套接字的连接。accept系统调用只有当客户程序试图连接到由socket参数指定的套接字上时才返回,也就是说,如果套接字队列中没有未处理的连接,accept将阻塞直到有客户建立连接为止。accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符,新套接字的类型和服务器监听套接字类型是一样的。它的原型如下:
int accept(int socket, struct sockaddr *address, size_t *address_len)
address为连接客户端的地址,参数address_len指定客户结构的长度,如果客户地址的长度超过这个值,它将会截断。
请求连接——connect系统调用
该系统调用用来让客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。它的原型如下:
int connect(int socket, const struct sockaddr *address, size_t address_len)
参数socket指定的套接字连接到参数address指定的服务器套接字。成功时返回0,失败时返回-1。
关闭socket——close系统调用
该系统调用用来终止服务器和客户上的套接字连接,我们应该总是在连接的两段(服务器和客户)关闭套接字。
进程使用流式socket进行通信
服务器程序:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int server_sockfd = -1;
int client_sockfd = -1;
int client_len = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//创建流套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器接收的连接地址和监听的端口
server_addr.sin_family = AF_INET; //指定网络套接字
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //接受所有IP地址的连接
server_addr.sin_port = htons(9736); //绑定到9736端口
//绑定(命名)套接字
bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_sockfd, 5); //创建套接字队列,监听套接字
signal(SIGCHLD, SIG_IGN); //忽略子进程停止或退出信号
while(1){
char ch = '\0';
client_len = sizeof(client_addr);
printf("Server waiting\n");
//接受连接,创建新的套接字
client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
if(fork()==0){
//子进程中,读取客户端发过来的信息,处理信息,再发送给客户端
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
else{
//父进程中,关闭套接字
close(client_sockfd);
}
}
}
这是一个服务器程序,它首先创建套接字,然后绑定一个端口再监听套接字,忽略子进程的停止消息等,然后它进入循环,一直循环检查是否有客户连接到服务器,如果有,则调用fork创建一个子进程来处理请求。利用read系统调用来读取客户端发来的信息,利用write系统调用来向客户端发送信息。这个服务器的工作非常简单,就是把客户发过来的字符+1,再发送回给客户。
客户端程序:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int sockfd = -1;
int len = 0;
struct sockaddr_in address;
int result;
char ch = 'A';
//创建流套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0); //设置要连接的服务器的信息
address.sin_family = AF_INET; //使用网络套接字
address.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器地址
address.sin_port = htons(9736); //服务器所监听的端口
len = sizeof(address);
result = connect(sockfd, (struct sockaddr*)&address, len); //连接到服务器
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
write(sockfd, &ch, 1); //发送请求给服务器
read(sockfd, &ch, 1); //从服务器获取数据
printf("char from server = %c\n", ch);
close(sockfd);
exit(0);
}
这是一个客户程序,它同样要先创建套接,然后连接到指定IP端口服务器,如果连接成功,就用write来发送信息给服务器,再用read获取服务器处理后的信息,再输出。
函数解释
socket
功能:创建一个套接口()。
原型:
#include <winsock.h>
SOCKET PASCAL FAR socket( int af, int type, int protocol);
af:一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式。
type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
SOCK_STREAM 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,为Internet地址族使用TCP。
SOCK_DGRAM 支持无连接的、不可靠的和使用固定大小(通常很小)缓冲区的数据报服务,为Internet地址族使用UDP。
SOCK_STREAM类型的套接口为全双向的字节流。对于流类套接口,在接收或发送数据前必需处于已连接状态。用connect()调用建立与另一套接口的连接,连接成功后,即可用send()和recv()传送数据。当会话结束后,调用closesocket()。带外数据根据规定用send()和recv()来接收。
实现SOCK_STREAM类型套接口的通讯协议保证数据不会丢失也不会重复。如果终端协议有缓冲区空间,且数据不能在一定时间成功发送,则认为连接中断,其后续的调用也将以WSAETIMEOUT错误返回。
- SOCK_DGRAM类型套接口允许使用sendto()和recvfrom()从任意端口发送或接收数据报。如果这样一个套接口用connect()与一个指定端口连接,则可用send()和recv()与该端口进行数据报的发送与接收。
htonl(), ntohl(), htons(), ntohs() 函数
在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。这是就可能用到htons(), ntohl(), ntohs(),htons()这4个函数。
网络字节顺序与本地字节顺序之间的转换函数:
htonl()–“Host to Network Long”
ntohl()–“Network to Host Long”
ntohs()–“Network to Host Short”
之所以需要这些函数是因为计算机数据表示存在两种字节顺序:NBO与HBO
网络字节顺序NBO(Network Byte Order): 按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。主机字节顺序(HBO,Host Byte Order): 不同的机器HBO不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。
如 Intel x86结构下, short型数0x1234表示为34 12, int型数0x12345678表示为78 56 34 12
如 IBM power PC结构下, short型数0x1234表示为12 34, int型数0x12345678表示为12 34 56 78
由于这个原因不同体系结构的机器之间无法通信,所以要转换成一种约定的数序,也就是网络字节顺序,其实就是如同power pc那样的顺序. 在PC开发中有ntohl和htonl函数可以用来进行网络字节和主机字节的转换.
int socket(int domain, int type,int protocol)
- domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等). AF_UNIX只能够用于单一的Unix系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程主机之间通信
-
type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等) SOCK_STREAM表明我们用的是TCP协议,这样会提供按顺序的,可靠,双向,面向连接的比特流. SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.
socket为网络通讯做基本的准备.成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
- sockfd:是由socket调用返回的文件描述符.
- addrlen:是sockaddr结构的长度.
- my_addr:是一个指向sockaddr的指针.
有 sockaddr的定义
struct sockaddr
{
unisgned short as_family;
char sa_data[14];
};
不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在其中有sockaddr_in的定义
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
我们主要使用Internet所以sin_family一般为AF_INET,sin_addr设置为INADDR_ANY表示可以和任何的主机通信,sin_port是我们要监听的端口号.sin_zero[8]是用来填充的. bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样
int listen(int sockfd,int backlog)
- sockfd:是bind后的文件描述符.
- backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度.
listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
- sockfd:是listen后的文件描述符.
- addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了.
bind,listen和accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接. accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
- sockfd:socket返回的文件描述符.
- serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址
-
addrlen:serv_addr的长度
connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符失败时返回-1
总的来说网络程序是由两个部分组成的–客户端和服务器端.它们的建立步骤一般是:
服务器端
socket–>bind–>listen–>accept
客户端
socket–>connect