最简单的一对一的服务端网络端通信(socket)
Socket=(IP地址:端口号),例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)
socket可以理解成计算机提供给程序员的接口,数据在客户端和服务端之间的socket之间传输。socket把复杂的TCP/IP协议封装,对于程序员来说只要利用好函数,就可以实现数据通信。
TCP提供了stream和datagram两种通信机制,所以socket分这两种。
stream的类型是SOCK_STREAM,采用TCP协议,TCP协议在计算机网络中是安全可靠的有连接的协议。datagram的类型是SOCK_DGRAM,采用的是UDP协议,UDP是不可靠的协议,现在在实际应用开发中,主要采用的是TCP。
服务端主要流程:创建socket—bind绑定ip和port—listen监听客户端连接请求—accept接受连接----数据传输----close关闭连接
int listenfd;
listenfd=socket(AF_INET,SOCK_STREAM,0);//在socket编程中,AF_INET是必须的,等同于固定搭配
//socket创建成功后如果返回值是-1的话,说明创建失败,为0的时候就是创建成功
if(listenfd==-1)
{
printf("socket create fail\n");
return -1;
}
struct sockaddr_in serveraddr;//定义一个用来处理网络通信的数据结构,sockaddr_in分别将端口和地址存储在两个结构体中
//sin_family协议族
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
//serveraddr.sin_addr.s_addr=atoi(argv[1]);// specify ip address
serveraddr.sin_port=htons(atoi(argv[1]));//specify port
//printf("%s %s\n",argv[1],argv[2]);
if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
{
printf("bind failed \n");
return -1;
}
INADDR_ANY 表示监听0.0.0.0地址,socket只绑定端口,不绑定本主机的某个特定ip,让路由表决定传到哪个ip(0.0.0.0地址表示所有地址、不确定地址、任意地址)(一台主机中如果有多个网卡就有多个ip地址)
htons()把short型值转成按网络字节顺序排列的short型值
htonl()把long型值转成按网络字节顺序排列的long型值
假设你已经有了一个sockaddr_in结构体ina,你有一个IP地址"132.241.5.10" 要储存在其中,你就要用到函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下:
ina.sin_addr.s_addr = inet_addr(“132.241.5.10”);
if(listen(listenfd,5)!=0)
{
printf("Listen failed\n");
close(listenfd);
return -1;
}
backlog 5 是未经过处理的连接请求队列可以容纳的最大数目。
int clintfd;//socket for client
int socklen=sizeof(struct sockaddr_in);
struct sockaddr_in client_addr;
clintfd=accept(listenfd,(struct sockaddr*)&client_addr,(socklen_t *)&socklen);
if(clintfd==-1)
printf("connect failed\n");
else
printf("client %s has connnected\n",inet_ntoa(client_addr.sin_addr));
inet_ntoa将网络地址转换成“.”点隔的字符串格式。
char buffer[1024];
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
iret=recv(clintfd,buffer,sizeof(buffer),0);
if (iret<=0)
{
printf("iret=%d\n",iret); break;
}
printf("receive :%s\n",buffer);
strcpy(buffer,"ok");//reply cilent with "ok"
if ( (iret=send(clintfd,buffer,strlen(buffer),0))<=0)
{
perror("send");
break;
}
printf("send :%s\n",buffer);
}
// 6th close socket
close(listenfd); close(clintfd);
对于服务端来说有两个socket,这里该如何理解呢?
一开始socket函数, 不管在客户端还是在服务端, 创建的都是主动socket, 但是在服务端经过listen(), 后把其转变为listen_socket_fd(被动监听socket),经过accept()后转变为connect_socket_fd(已连接socket).
在转变为connect_socket_fd之前, 都是同一个socket, 只不过是socket的状态改变了, 但是服务端经过accept()后返回的socket是新的socket, 用于连接后的read()/write()
假设只用一个socket完成整个过程. 那么这个socket就会一直被占用, 而不能被另外的客户端请求, 造成了服务端的性能极其低下, 如果没有存储后面的客户端请求, 就会被错过而丢弃, 因为当前的socket正在与当前一个客户端的socket建立连接.
客户端 socket-connect
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // server's port
servaddr.sin_addr.s_addr=inet_addr(argv[1]);//server's ip
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // send request to server for connection
{
perror("connect");
close(sockfd);
return -1;
}
多进程的(一个服务端服务多个客户端)
具体实现就是:在accept部分增加一个外层的循环,
子进程执行完之后就return 0或者exit(0),直接退出去。
还有一个问题就是:查看打开的进程编号可以发现,父进程和子进程文件描述符一样的。对于父进程来说,只需要监听连接,不需要连接后的fd;对子进程来说,listenfd也是不需要的。但是fork会生成一份副本,导致父子进程都有listenfd和connectfd,所以可以在父进程中关掉connectfd,子进程中关掉listenfd。为什么要这么做,因为fd是一种资源,维护一个fd不难,但是很多客户端就会有很多冗余的fd,造成资源浪费。而且一个进程能打开的fd是有限的。
多进程服务程序的退出
子进程收到退出信号(ctrl+c),在子进程设置signal(2,childexit),signal(15,childexit)。然后子进程关闭客户端的fd再退出。
父进程会关闭listenfd,然后通知所有子进程退出,然后退出。
TCP长连接和心跳机制的实现
短连接:
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接(管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段);
连接→数据传输→关闭连接;
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(不发生RST包和四次挥手)。
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接(一个TCP连接通道多个读写通信);
长连接多用于操作频繁(读写),点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接(http1.0只支持短连接,1.1keep alive 带时间,操作次数限制的长连接),因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好;
具体实现:
服务端和客户端约定超时时间,假设35秒;
如果服务端35秒没收到报文,认为客户端异常,主动关闭;
客户端35秒内没有任何业务,发送心跳报文,以维持连接。
客户端在空闲时发送心跳报文,服务端的读函数增加一个超时参数,超时就断开连接,不超时就回复成功。
实现文件的上传和下载功能、异步通信机制实现快速传输。
主要包含:文件传输的服务端模块(支持上传下载) 文件上传的客户端,文件下载的客户端。
客户端分成两个是因为让程序结构更简单,模块化;服务端不分开因为它是网络服务程序,分成两个的话就要两个监听的端口,配置网络参数更麻烦,比如路由器防火墙。
文件上传客户端:登录(意义在于协商与服务端协商文件传输参数,最重要参数就是文件存放目录,客户端的A目录存在服务端的B目录)
客户端获取本地目录的文件清单,假设有100个文件,一个循环,每次先传文件信息(名字,大小,时间),再传内容。
结束后客户端休息几秒,再重新获取本地目录的文件清单。
注意:用于系统内部文件传输,所以客户端程序常驻内存,每隔几秒就传输一次。客户端上传成功后直接删除本地文件就可以。
定义一个结构体存储心跳,目录等信息。
服务端只要之前的参数就可以,客户端通过登录报文将更多的参数传到服务端。
服务端再修改相应的代码。处理登录报文,如果是1,就完成上传功能主函数。
首先服务端调用_xmltoarg方法解析登录报文,如果是1或者2,发送ok给客户端,否则发送failed给客户端。如果登陆失败(美没收到服务端回应),直接退出。
如果类型为1,调用RecvFilesMain()
void RecvFilesMain()
{
while(true)
{
memset(recvbuf,0,sizeof(recvbuf));
memset(sendbuf,0,sizeof(sendbuf));
如果接收客户端的报文read失败(超时),记录日志,return;
如果接收缓存内容是心跳报文,给客户端回复ok;
处理上传文件的请求报文
}
}
客户端文件信息的上传怎么实现?
服务端怎么处理这些信息
传输文件的内容
sendfile函数实现。参数有连接的socketfd,文件名,文件大小。
异步通讯三种实现方法:
1、多进程:服务端客户端后,fork一个子进程,子进程负责接收信息,父进程发送信息,
2、多线程。
3、I/O复用技术
同步的话客户端发送报文,要等到接受ok再发送第二个,10000个数据需要6秒。异步的话,父进程发送报文,子进程接受ok的成功信息,1秒。
如果是10万的话,同步每秒2000左右,异步每秒8万左右。
I/O复用就是没有数据的话不等待,直接返回。每秒差不多6万,低一点,原因就是有数据的话会等待数据搞完再发送,当然低一点。