目录
一、什么是Socket?
二、socket通信流程
Server端
一、创建socket
二、命名socket。
三、绑定
四、监听
五、关闭
Client端
一、创建socket
二、connect
三、发送数据
四、关闭socket
套接字除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。本文主要介绍本地进程间socket通信。
我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
socket API原本是为网络通讯设计的,可以用于在网络上传送数据,换言之,可实现不同机器上的进程通信过程。但后来在socket的框架上发展出一种IPC机制(IPC:即进程间通信),就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1)。但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。
UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,比如X Window服务器和GUI程序之间就是通过UNIX Domain Socket通讯的。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示(网络socket通信用别的结构体 sockaddr_in),网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
创建socket,类型为AF_LOCAL或AF_UNIX,表示用于进程通信。创建套接字需要使用 socket 系统调用,其原型如下:
int socket(int domain, int type, int protocol);
socket结构体:
struct socket
{
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
由此,我们清楚了,socket结构体不仅仅记录了本地的IP和端口号,还记录了目的IP和端口。
这一步对本地进程间通信非常重要,SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 路径名 */
};
这里面有一个很关键的东西,socket进程通信命名方式有两种
//name the server socket
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path,"/tmp/UNIX.domain");
server_len = sizeof(struct sockaddr_un);
client_len = server_len;
抽象命名空间,这种方式不需要创建socket文件,只需要命名一个全局名字,即可让客户端根据此名字进行连接。后者的实现过程与前者的差别是,后者在对地址结构成员sun_path数组赋值的时候,必须把第一个字节置0,即sun_path[0] =
//name the socket
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SERVER_NAME);
server_addr.sun_path[0]=0;
server_len = strlen(SERVER_NAME) + offsetof(struct sockaddr_un, sun_path);
绑定要使用 bind 系统调用,把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。网络编程的socket地址是IP地址加端口号,而本地进程间通信是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。其原形如下:
int bind(int socket, const struct sockaddr_un* address, size_t address_len);
int server_sockfd = socket(AF_UNIX,SOCK_STREAM,0);
bind(server_sockfd, (struct sockaddr_un *)&server_address, sizeof(sockaddr_un));
服务器端套接字创建完毕并赋予本地地址值后,需要进行监听,等待客户端连接并处理请求,监听使用 listen 系统调用,接受客户端连接使用accept系统调用,它们的原形如下:
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr_un*address, size_t *address_len);
和下面的接口相同
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
accept函数主要用于服务器端,一般位于listen函数之后,默认会阻塞进程,直到有一个客户请求连接,建立好连接后,它返回的一个新的套接字 socketfd_new ,此后,服务器端即可使用这个新的套接字socketfd_new与该客户端进行通信,而原来的sockfd 则继续用于监听其他客户端的连接请求。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。
备注:新建立的套接字准备发送send()和接收数据recv()。
至此,我的困惑产生了,这个新的套接字 socketfd_new 与监听套接字sockfd 是什么关系?它所代表的socket对象包含了哪些信息?socketfd_new 是否占用了新的端口与客户端通信?当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢?
客户端发送过来的数据可以分为2种,一种是连接请求,一种是已经建立好连接后的数据传输。
由于TCP/IP协议栈是维护着一个接收和发送缓冲区的。在接收到来自客户端的数据包后,服务器端的TCP/IP协议栈应该会做如下处理:如果收到的是请求连接的数据包,则传给监听着连接请求端口的socetfd套接字,进行accept处理;如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new 套接字通过recv或者read函数到缓冲区里面去取指定的数据(因为socketfd_new代表的socket对象记录了客户端IP和端口,因此可以鉴别)。
#define MAX_CONNECTION_NUMBER 10
int server_client_length, server_client_sockfd;
struct sockaddr_un server_client_address;
listen(server_sockfd, MAX_CONNECTION_NUMBER);
while(1)
{
// ...... (some process code)
server_client_length = sizeof(server_client_address);
server_client_sockfd = accept(server_sockfd, (struct sockaddr*)&server_client_address, &server_client_length);
// ...... (some process code)
}
read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,I/O操作有下面几组:
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
int close(int socket);
这一步同server端,获取client端socket的描述字。client和server的socket创建一般是先server端创建,再client端创建。
客户端套接字创建完毕并赋予本地地址值后,需要连接到服务器端进行通信,让服务器端为其提供处理服务。对于 SOCK_STREAM 类型的流式套接字,需要客户端与服务器之间进行连接方可使用。连接成功后,就可以利用这个socketfd描述符使用send/recv函数收发数据了。连接要使用 connect 系统调用,其原形为
int connect(int socket, const struct sockaddr_un*address, size_t address_len);
send()
为什么send函数仅仅传入sockfd就可以知道服务器的ip和端口号?
其实,socket的结构体我们已经很清楚了,sockfd 描述符所描述的socket对象不仅包含了本地IP和端口,同时也包含了服务器的IP和端口,这样,才能使得send函数只需要传入sockfd 即可知道该把数据发向什么地方。而代码中,目的IP和端口只是在connect函数中出现过,因此,肯定是connect函数在成功建立连接后,将目的IP和端口写入了sockfd 描述符所描述的socket对象中。
这一步同server端