最近在看redis源码,发现它的网络通信部分是用到自己封装的anet框架,把底层的socket的建立、处理等都封装起来。除此之外,在客户端与服务器端之间的通信协议也不是简单的基于TCP/IP的使用,也是通过自己实现的协议满足自己的使用场景。于是对底层的socket通信想了解一下,就看了相关内容,了解socket之间的通信是如何进行。
socket介绍
socket可以理解成是一种特殊的接口,这种特殊的接口是面向进程进行信息的传递和交互一种特殊方式。通过socket,可以在一个主机进程之间或者不同主机之间进程的通信。现在大部分的网络之间的通信底层都是基于socket进行信息的交互。大部分的网络框架也是通过封装socket通信对外暴露更友好的接口提供更丰富的功能。
当使用socket进行主机之间通信,基于socket的应用开发是处于TCP/IP中的传输层之上进行数据传递的。在传输层锁使用的协议有TCP和UDP,所以通信根据协议划分可分为基于流的TCP通信和基于数据报的UDP通信。两个协议之间的区别大家都知道一个是面向连接一个是面向无连接的。因为此差别,在开发的时候也会有不同的处理方式。
下面看到一个不错的图片很清晰的解释了socke所处于的层次:
可以看到通过socket接口层,底层的传输协议的不同就可以被屏蔽掉,直接面向接口层进行编程。接下来就简单讲述socket的具体实现。
socket实现
当请求更多的时候,每个请求处理都产生一个进程或者线程的话,那么内核在进程之间切换和进程资源回收的时候,所耗费的时间也会对服务端的性能产生影响。这个时候就需要更加快速的服务端实现。比如select方式等。
简单的C/S实现
在这种模式下,就是简单的实现服务端接受客户端请求,然后返回客户端数据,处理结束。服务端继续上面流程,直到程序退出。
server
//head file
#ifndef SERVER_H
#define SERVER_H
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <iostream>
#include <string.h>
using namespace std;
class Server
{
public:
Server();
int start_server();
private:
const static int port;
};
#endif
//cpp
#include "s_server.h"
const int Server::port = 9191;
Server::Server()
{}
int Server::start_server()
{
int length;
int sock, sock_peer;
struct sockaddr_in server;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0) cerr<< "creat sock failed" << endl;
length = sizeof(server);
memset(&server, 0, length);
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(port);
if(bind(sock, (struct sockaddr *)&server, length) < 0)
cerr << "failed bind port" << endl;
listen(sock, 5); //5 is the max pending connection,if pending num bigger than 5, return ECONNREFUSED to client
int len;
while(1) {
sock_peer = accept(sock, (struct sockaddr *) NULL, NULL);
if(sock_peer < 0)
cerr << "failed accept peer connection request" << endl;
write(sock_peer, "test", 4);
close(sock_peer);
}
}
int main(){
Server s;
s.start_server();
return 1;
}
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
int port = 9191;
int length;
int sock;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0) cerr<< "creat sock failed" << endl;
struct sockaddr_in server;
length = sizeof(server);
memset(&server, 0, length);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("127.0.0.1");
server.sin_port = htons(port);
if(connect(sock, (struct sockaddr *)&server, length) < 0)
cerr << "connect server failed!!!" << endl;
char buf[256];
read(sock, buf, 255);
cout << buf<< endl;
}
上面代码就实现了最简单的一个基于TCP的socket通信,一个服务器接受客户端请求,一个客户端发送请求。
struct sockaddr_in
{
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; /* Not used, must be zero */
};
通过注释可以很容易理解到每个字段的含义。这里主要是介绍一下in_addr字段,这个字段是用来说明创建socket以后,该socket具体监听哪个ip和端口号。因为你创建一个socket以后必须告诉系统,该socket监听哪个ip、哪个端口号。在单网卡上的机器上其实没什么需要注意的。但是有些服务器是有很多网卡的,所以这个字段必须指明监听细节。一般都是直接赋值为INADDR_ANY,这个在机器上都是0.0.0.0,意思是监听本机所有网卡上的ip。
多进程实现
上面是一个最简单的基于socket的C/S的实现,服务器启动以后阻塞住开始监听端口,当有请求来的时候在当前进程处理请求,然后返回客户端数据。像上面讲解到,当该处理需要花费较长时间的时候,客户端就会长时间接受不到服务器返回数据。除此之外,当其他新的请求到来的时候一会阻塞住无法处理数据。根据此类情况,服务端的设计必须优化,以更快速的响应客户端的请求。
接下来就讲解一种多进程的实现。当一个请求到来服务端接受的时候,服务端会fork一个进程去处理当前请求。然后服务端继续监听端口,接受其他新的请求。通过多进程实现,就可以避免当服务端处理一个请求的时候,无法接受新的请求的瓶颈。
int Server::start_server_p()
{
int length;
int sock, sock_peer;
struct sockaddr_in server;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0) cerr<< "creat sock failed" << endl;
length = sizeof(server);
memset(&server, 0, length);
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(port);
if(bind(sock, (struct sockaddr *)&server, length) < 0)
cerr << "failed bind port" << endl;
listen(sock, 5);
int len;
while(1)
{
sock_peer = accept(sock, (struct sockaddr *) NULL, NULL);
if(sock_peer < 0)
cerr << "failed accept peer connection request" << endl;
int pid = fork();
if(pid == -1)
cerr << "create process faild!!!" << endl;
else
{
if(pid == 0)
{
close(sock);
handle_req(sock_peer);
exit(0);
}
else
{
// in parent process, the pid is child pid
cout << "pid:" << pid << endl;
}
}
}
}
从上述代码 可以看到socket的在服务器端的流程是创建 socket->bind->listen->accept->handle req 变为了socket->bind->listen->accept->new process handle。accept成功之后创建一个进程处理,不影响原来的服务进程接受新的请求。
基于select实现
int Server::start_server_s()
{
int length;
int sock, sock_peer;
struct sockaddr_in server;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0) cerr<< "creat sock failed" << endl;
length = sizeof(server);
memset(&server, 0, length);
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(port);
if(bind(sock, (struct sockaddr *)&server, length) < 0)
cerr << "failed bind port" << endl;
listen(sock, 5);
if(-1 == fcntl(sock, F_SETFD, O_NONBLOCK))
{
cerr << "fcntl error!!!";
exit(1);
}
fd_set master, readfds;
FD_ZERO(&master);
FD_ZERO(&readfds);
FD_SET(sock, &master);
cout<< "sock is :" << sock << endl;
int maxfd = sock;
int ready = -1;
int bytes = -1;
char buf[256];
while(1)
{
memcpy(&readfds, &master, sizeof(master));
if(-1 == (ready = select(maxfd + 1, &readfds, NULL, NULL, NULL)))
{
cerr << "select sock failed!!!" << endl;
exit(0);
}
cout<< "ready: " << ready << ", sock: " << sock << ", maxfd:" << maxfd << endl;
for(int i = 0; i <= maxfd && ready > 0; i ++)
{
if(FD_ISSET(i, &readfds))
{
ready --;
if(i == sock)
{
cout << "accpet new sock, the i is " << i << endl;
int sock_peer = accept(sock, NULL, NULL);
if(sock_peer < 0)
{
cerr << "accept sock_peer failed!!!" << endl;
exit(0);
}
if(-1 == fcntl(sock, F_SETFD, O_NONBLOCK))
{
cerr << "fcntl sokc_peer error!!!";
exit(1);
}
FD_SET(sock_peer, &master);
if(maxfd < sock_peer)
maxfd = sock_peer;
}
else
{
cout << "recv data, the i is " << i << endl;
handle_req(i);
FD_CLR(i, &master);
}
}
}
}
}
- fd_set:一个存放文件描述符的数据结构,其实底层是一个数组。在IO复用处理过程中是对这个数据结构做相应操作
- FD_SET:宏定义,注意是大写,这个是对fd_set进行添加操作
- FD_ZERO:宏定义,讲fd_set清空,不包含任何文件描述符
- FD_CLR:宏定义,从fd_set中删除一个给定的文件描述符
int select(int maxfd, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
分别对每个参数进行简单介绍。
最后一个参数timeout是告诉内核等待的时间,当为NULL的时候,则标识永远监听等待事件的发生;当值为0的时候,则不等待测试完所有描述符立即返回;不为0的时候,那么在超过等待时间或者已经有事件ready的时候返回。
基于epoll的实现
资料补充
在socket开发中,一些用到的方法和数据结构在这里作一个简单的介绍。
socketaddr vs socketaddr_in
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
这两个数据结构都封装了socket在connct、bind等相关方法中所需要的ip、端口号和协议等相关内容。区别是sockaddr_in封装了ipv4相关的属性,还有sockaddr_in6是对于ipv6的相关属性的定义。在实际的底层协议中除了基于ipv4和ipv6的端口实现之外,还有其他各种协议实现。对于底层方法不可能对每种协议都有一个方法进行处理。为解决ipv4和ipv6不同的数据结构的问题,出现了sockaddr结构体。通过该结构体对所有的协议结构体进行抽象,对于bind、connect等方法都是接受sockaddr指针作为参数,而不是sockaddr_in或者sockaddr_in6。这也导致问题,在调用socket相关函数需要传递相关结构体的时候需要先对从参数进行强制类型转换,转换成socketaddr形式的结构体;否则就会给出出错或者警告信息。
除了这两种结构体之外,其实还存在另外一种结构体addrinfo,它是通过getaddrinfo方法,可以获取相应的参数。在addrinfo数据结构中也封装有socket建立相关属性。这三个数据结构,在不同的场合下可以使用不同的数据结构。
write vs send vs sendto
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t write(int sockfd, const void *buf, size_t len, int flags);
其中上面的三个函数是只面向socket的函数,而write则不一定是只使用在socket中。在所有的文件读写中都可以使用write函数,只要传入一个文件的描述符就可以,无论这个描述符是一个sock还是一个真是的文件fd。
虽然是两种不同类型的函数,但是他们之间在使用上也有一定的共同点。有时候通过一些参数的控制,不同函数之间其实在原理上就达到了同样的效果。比如 针对send与write函数来说,当send的falgs参数为0的时候,那么send函数与write函数在功能上则是相同的,都是向一个socke写数据。对于send和sendto在一定程度上也有相同之处。当两个方法参数在某种情况下的时候send与sendto所起到的作用是一样的。
recv vs recvfrom vs recvmsg
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);