Cpp实现CS架构
本文最后更新于:9 天前
阿里嘎多,来为计网实验做个准备
本文代码运行环境为 linux系统
你可以在我的github仓库中找到源代码,或者在本文底部 可以star一波 小声bb
要构建一个应用程序
首先要选择它的结构,有两种典型架构: CS Client-Server 和 P2P peer to peer。其中CS架构是我们最常接触和使用到的。
接下来我们要实现进程间通信。 这就是今天我们要干的事情:在CS架构下实现 Client与server的通信。
网络进程间通信是通过 socket(应用层与运输层之间的接口) 来向网络 发送/接收 报文实现的。 大多数语言都为我们提供了socket API。 我们能做的就是 使用这些API,选择运输层参数,实现通信。
UDP实现CS
在这部分我们实现一个 server将client发送内容转化为大写并返回的功能
UDP_server
首先分析一下 UDP协议下server需要做什么。
- 创建socket,绑定自己的ip和端口。 这是server和client都需要做的 应用层必须按协议栈向下运行才能实现通信。而socket正是这个应用层与下层的接口。
- 从socket读取client发送来的内容。
- 通过socket,确定目的地(client的ip和端口号),发送响应内容。
接下来我们来一步步实现
创建、初始化socket
//创建Udp_socket,socket函数来自 sys/socket.h, IPPOROTO_UDP来自netinet/in.h
int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
socket函数原型为:
int socket(int af, int type, int protocol);
af代表地址族(IP),一般为ipv4或ipv6
其中AF_INET代表IPV4 ip地址,SOCK_DGRAM表示是无保障传输,protocol即运输协议。
我们在这里就是声明了一个UDP的socket
为了能让client能访问到我们,我们使用bind函数进行绑定,将server的套接字与一个IP和一个端口相连。
struct sockaddr_in server_addr;
// struct sockaddr_in{
// sa_family_t sin_family; //地址族(Address Family),也就是地址类型
// uint16_t sin_port; //16位的端口号
// struct in_addr sin_addr; //32位IP地址
// char sin_zero[8]; //不使用,一般用0填充
// };
// struct in_addr{
// in_addr_t s_addr; //32位的IP地址
// };
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(1234);//转为大端序
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//function from arpa/inet.h
bind(Udp_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));
这里一下子多了很多内容,我们来看一下。
首先是结构体 sockaddr_in,他的内容已在上面代码写出。
首先是 sin_family成员,和sock函数的第一个参数含义相同,取值也要相同。IPV4, AF_INET
sin_port为要绑定的端口号。理论取值为 0 ~65536,但因为 0 ~1023端口一般都分配给特点的应用,所 以尽量避免使用。 可以在这个范围里随意选一个
sin_addr竟然也是一个结构体…用来绑定IP
bind函数原型:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
关于sockaddr和sockaddr_in的区别详见: 这里
准备接受数据
芜湖,我们终于结束了折磨人的初始化环节,接下来就简单了!
我们首先要申请缓冲区来读写数据,并申请一些变量。就像Linux中通过文件描述符读写文件也需要缓冲区一样
char recv_buf[50];//接受缓冲区
char send_buf[50];//发送缓冲区
int recv_num;//接收字节数
int send_num;//发送字节数
struct sockaddr_in client_addr;//客户地址
int len=sizeof(client_addr);//客户地址长度
为了将数据发送给client,我们也需要他的IP和端口号这就是在这里声明一个 client_addr的原因。
开始接/发数据
while(true){
cout<<"waiting for data"<<endl;
//从client端接收数据
recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&client_addr,(socklen_t *)&len);
//检验错误
if(recv_num<0)
{
cout<<"receive ERROR"<<endl;
exit(1);
}
recv_buf[recv_num]='\0';//添加字符串结束符
cout<<"Receive "<<recv_num<<"bytes: "<<recv_buf<<endl;
//实现server功能,即转化为大写。
for(int i=0;i<recv_num;i++){
if(recv_buf[i]>='a'&&recv_buf[i]<='z')
send_buf[i]=toupper(recv_buf[i]);
}
send_buf[recv_num]='\0';
//发送转化完的数据
send_num=sendto(Udp_socket,send_buf,recv_num,0,(struct sockaddr*)&client_addr,len);
if(send_num<0)
{
cout<<"send ERROR"<<endl;
exit(1);
}
}
可以看到使用了recvfrom和sendto两个函数,分别用来接、发数据
recvfrom函数原型
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from,int *fromlen);
s为socketfd,即server当前使用的套接字
buf为接收缓冲区,即声明的数组
len为缓冲区大小,一般使用sizeof表示
flags参数一般不用,设置为0
from为client端地址,使用 *(struct sockaddr)&client_addr**的形式
fromlen为指针,指向from缓冲区长度值。
sendto函数原型
int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
与recvfrom类似,只是将buf(接收缓冲区)换成了msg(发送缓冲区);将fromlen指针直接换成了长度参数。
最后只需要关闭socket。(非常像fd!)
close(Udp_socket);
UDP_client
与server类似,我们也先来分析一下client需要干什么事情。
- 创建socket(有socket才能通信),绑定server的ip和端口。
- 准备发送数据。
- 通过socket发送数据,并接收反馈数据。
创建、初始化socket
这部分client和server有很大的不同。
在server中,我们首先将socket与自己的IP和端口绑定了;但在client中,我们并没有声明任何有关自己的地址变量。
目前可以从 UDP的特点进行解释:
UDP是不面向连接的传输协议,每一个socket由一个二元组(目的IP和目的Port)决定,而且在server中,通过recvfrom函数是可以获得client的IP和Port的。
所以此时我们只要确定目的IP和Port就可以唯一在server中确定一个socket来接受数据。
注意:这样的情况只有在CS架构下才可能实现:
如果在P2P架构下,每个人都会是server,如果不绑定自己的IP和Port,别就无法向你发送数据。
而CS架构下由于client一直为发出数据后再接收;server一直为 接收数据后再发送。所以server并不需要自己知道自己的IP和Port,只需要让server知道即可。
int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(1234);
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
准备接受数据
创建缓冲区……balabala….
int len=sizeof(server_addr);
char send_buf[50];
char recv_buf[50];
int send_num,recv_num;
cout<<"please input string!"<<endl;
cin>>send_buf;
开始发/接数据
和上文中server类似,只不过是收发顺序颠倒一下,少了一个功能实现部分。
send_num=sendto(Udp_socket,send_buf,strlen(send_buf),0,(struct sockaddr*)&server_addr,len);
if(send_num<0){
cout<<"send ERROR"<<endl;
exit(1);
}
recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf),0,(struct sockaddr*)&server_addr,(socklen_t*)&len);
if(recv_num<0){
cout<<"receive ERROR"<<endl;
exit(1);
}
recv_buf[recv_num]='\0';
cout<<"Server give back: "<<recv_buf<<endl;
close(Udp_socket);
return 0;
}
TCP实现CS
TCP协议与UDP协议的一大不同就是: TCP时面向连接传输层协议,而UDP是不面向连接的。
二者的类比就像是: TCP是打电话,UDP是写信。即一个会建立专线,另一个和大家共享资源。
UDP协议只是在IP上面很简单地封装了一层,而TCP要更加复杂。
不管怎么,在这篇博客中我们并不需要关心底层,我们只需要知道API就行了 :happy:
另外,请记住,TCP是面向连接的!
TCP_Client
相信看过UDP程序后你对socket编程已经很有心得了,本部分我们就简单地实现一个server在收到消息后简单地print一句话给client的naive程序。
你会注意到这次我先编写的Client程序,原因是TCP协议下用户和服务器连接需要用户先于服务器 “握手” 创建连接,这样的顺序应该会更符合逻辑
创建、初始化socket
注意将 socket()函数中的type和protocol参数更改为稳定数据传输和TCP
其他和UDP_client的内容基本相同,也是不需要绑定自己的IP和Port
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
创建连接&准备发/收数据
由于我们实现的功能没有发数据,所以就少了一些内容。
注意,connect函数就是client用来和server建立连接的过程
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//读取服务器传回的数据
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//关闭套接字
close(sock);
return 0;
connect函数原型:
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)
sockfd即指定数据发送的套接字;
server_addr即指定数据发送地址;
addrlen即指定server_addr结构体的长度;
然后我们就可以通过read函数将取服务器发回的数据读入缓冲区中
TCP_Server
创建、初始化socket
注意将 socket()函数中的type和protocol参数更改为稳定数据传输和TCP
其他和UDP_server的内容基本相同。
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
接受数据
由于我们实现的功能只有print,所以就不需要预先设定缓冲区
//进入监听状态,等待用户发起请求
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据
char str[] = "hello client!";
write(clnt_sock, str, sizeof(str));
这里出现了listen函数,即监听函数,这是对client中connect函数的一个照应。
listen函数原型:
int listen ( int sockfd, int backlog );
sockfd即被 listen函数作用的套接字。是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数。
backlog 即server同时建立连接的上限值。
然后同accept函数接受client的请求
accept函数作用:接收一个套接字中已建立的连接。
原型:
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
emmm,相信大家都能看懂,不然就来这。
发送数据
//向客户端发送数据
char str[] = "hello client!";
write(clnt_sock, str, sizeof(str));
//关闭套接字
close(clnt_sock);
close(serv_sock);
return 0;
}
通过write向client写数据。
最后关闭socket即可。
Source code
UDP_server
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
using namespace std;
int main()
{
//创建Udp_socket,socket函数来自 socket.h, IPPOROTO_UDP来自in.h
int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
struct sockaddr_in sever_addr;
// struct sockaddr_in{
// sa_family_t sin_family; //地址族(Address Family),也就是地址类型
// uint16_t sin_port; //16位的端口号
// struct in_addr sin_addr; //32位IP地址
// char sin_zero[8]; //不使用,一般用0填充
// };
memset(&sever_addr,0,sizeof(sever_addr));
sever_addr.sin_family=AF_INET;
sever_addr.sin_port=htons(1234);
sever_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//function from arpa/inet.h
bind(Udp_socket,(struct sockaddr*)&sever_addr,sizeof(sever_addr));
cout<<"Sever is ready to response!!!"<<endl;
char recv_buf[50];//接受缓冲区
char send_buf[50];//发送缓冲区
int recv_num;
int send_num;
struct sockaddr_in client_addr;
int len=sizeof(client_addr);
while(true){
cout<<"waiting for data"<<endl;
recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf)-1,0,(struct sockaddr*)&client_addr,(socklen_t *)&len);
if(recv_num<0)
{
cout<<"receive ERROR"<<endl;
exit(1);
}
recv_buf[recv_num]='\0';
cout<<"Receive "<<recv_num<<"bytes: "<<recv_buf<<endl;
for(int i=0;i<recv_num;i++){
if(recv_buf[i]>='a'&&recv_buf[i]<='z')
send_buf[i]=toupper(recv_buf[i]);
}
send_buf[recv_num]='\0';
send_num=sendto(Udp_socket,send_buf,recv_num,0,(struct sockaddr*)&client_addr,len);
if(send_num<0)
{
cout<<"send ERROR"<<endl;
exit(1);
}
}
close(Udp_socket);
return 0;
}
UDP_Client
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
using namespace std;
int main()
{
int Udp_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
struct sockaddr_in sever_addr;
memset(&sever_addr,0,sizeof(sever_addr));
sever_addr.sin_family=AF_INET;
sever_addr.sin_port=htons(1234);
sever_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
int len=sizeof(sever_addr);
char send_buf[50];
char recv_buf[50];
int send_num,recv_num;
cout<<"please input string!"<<endl;
cin>>send_buf;
send_num=sendto(Udp_socket,send_buf,strlen(send_buf),0,(struct sockaddr*)&sever_addr,len);
if(send_num<0){
cout<<"send ERROR"<<endl;
exit(1);
}
recv_num=recvfrom(Udp_socket,recv_buf,sizeof(recv_buf),0,(struct sockaddr*)&sever_addr,(socklen_t*)&len);
if(recv_num<0){
cout<<"receive ERROR"<<endl;
exit(1);
}
recv_buf[recv_num]='\0';
cout<<"Sever give back: "<<recv_buf<<endl;
close(Udp_socket);
return 0;
}
TCP_Client
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//读取服务器传回的数据
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//关闭套接字
close(sock);
return 0;
}
TCP_server
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//进入监听状态,等待用户发起请求
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据
char str[] = "hello client!";
write(clnt_sock, str, sizeof(str));
//关闭套接字
close(clnt_sock);
close(serv_sock);
return 0;
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!