作为网络编程的入门,我根据计通网的大纲从OSI的七层模型入手,尝试去逐步理解网络分层模型的搭建以及网络分层的必要性,并且利用一个网元的模型设计和编程实现,我们将模型浓缩为应用层,网络层和物理层。利用套接字库实现不同网络层次间的通信连接,并且利用端口号的匹配,实现了我设计开发的程序与电子科技大学网络工程系计算机网络课程组的物理层比特流传输模拟器的对接。以下对我们这次网元模型设计进行仔细分层次的分析与探讨。
如图所示是本次模型维护的网络模型框架,由应用层,网络层,物理层组成。
综上所述,网络层、应用层、物理层在同一台主机上利用端口号进行识别和匹配,两台主机间的物理层利用ip地址+端口号进行建立连接。 端口号在此次模型搭建中是用户在初始化的时候自定义给应用层或者网络层模块的。
如图所示,在应用层,我们将代码分为三个部分:初始化(包括本机端口号的设置,网络层端口号的匹配),Socket的创建和绑定以及与网络层间的通信处理。以下对这三个部分展开详细说明。
(1)初始化。在初始化阶段,我们会请求用户手动给应用层模块分配端口号。值得注意的是,得到用户输入的端口号后,会立即创建socket,并将申请的端口号绑定(bind)到该socket上。这一步的作用是判断用户申请的端口号是否已被占用,若已被占用才能及时让用户重新输入一个未被占用的端口号。此部分代码如下所示
printf("/---2019年春季段老师班聊天室模型--/\n");
printf("应用层模拟器\n");
printf("请设置本模块的端口号:");
scanf_s("%d", &myPort);
//创建socket
SOCKET socClient = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET是地址协议,代表Ipv4;参数二是套接字类型,SOCK_DGRAM是数据报套接字(udp通信)
if (socClient == SOCKET_ERROR) {
return 0;
}
remote_addr.sin_family = AF_INET;
remote_addr.sin_port = htons(myPort);
remote_addr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
/*
INADDR_ANY是ANY,是绑定地址0.0.0.0上的监听, 能收到任意一块网卡的连接;
INADDR_LOOPBACK, 也就是绑定地址LOOPBAC, 往往是127.0.0.1, 只能收到127.0.0.1上面的连接请求,127.0.0.1即回环地址
*/
while (bind(socClient, (sockaddr*)&remote_addr, sizeof(remote_addr)) != 0) {
printf("本模块的端口号已被占用,请换一个:");
scanf("%d", &myPort);
remote_addr.sin_port = htons(myPort);
}
sock_list.MainSock = socClient;//套接点设置成功
printf("请输入所连接的网络层模块端口号:");
scanf("%d", &sock_list.MainPort);
其中值得注意的是,
①代码中的remote_addr结构体是一种通用的地址信息格式,其元素包括了协议族、端口号、ip地址连接等的定义。在本次实验中,我们用的是ipv4的协议族,因此传入AP_INET。端口号传入网络层的端口号(初始化时传入的是本机端口号,用户输入网络层端口号后更新到该结构体中)。INADDR_LOOPBACK指的是127.0.0.1这样一个回环地址,由于是在本机内的不同模块间进行通信,模块间享用同张网卡,因此ip地址相同,仅仅利用端口号来对彼此进行区分。
②Bind函数在端口号和socket绑定成功后会返回0,不成功则不为0。因此在while的判断条件中利用bind函数的返回值判断是否绑定成功,巧妙地让用户输入端口号直到端口号设置成功为止。
(2)socket的创建和绑定如(1)所述,若用户成功输入一个未被占用的端口号,则bind端口后立即将该端口更新到套接字句柄结构体Sock_list中的MainSock。Sock_list是存储Socket信息的结构体,在后面的通信部分会将它作为形式参数传入函数中。Socket创建和绑定完成后,初始化过程完成,进入通信阶段。
(3)完成初始化后,应用层进入交互阶段,在界面上会提醒用户输入一段测试数据,输入完成后,程序会进入非阻塞的循环中,我的理解是同时进行监听和发送。利用Select的特点,在代码中可以设置一个时间参数,传入Select中,Select函数会在没有响应是会等待用户输入的时间值,超过该时间自动返回继续执行。一般这个时间参数设置为10ms级别的长度。当然如果想设置成为阻塞模式就把NULL传入此形式参数即可。此部分代码如下所示
while (1) {
time_t rawtime;
struct tm * timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
make_fdlist(&sock_list, &readfds);
//本模型采用了基于select机制,不断发送测试数据,和接收测试数据。
selretval = select(0, &readfds, &writefds, &exceptfds, &timeout);
if (selretval == SOCKET_ERROR) {
retval = WSAGetLastError();
break;
}
//检查是否按下空格加回车
if (getchar() == ' ') {
printf("\n\n---------------------发送栏----------------------\n\n");
printf("发送数据: ");
scanf("%s", sendbuf);
printf("发送时间:%s\n", asctime(timeinfo));
remote_addr.sin_port = htons(sock_list.MainPort);//设置目标端口号
//发送一份测试数据到接口0
retval = sendto(sock_list.MainSock, sendbuf, strlen(sendbuf) + 1, 0, (sockaddr *)&remote_addr, sizeof(remote_addr));
if (retval > 0)
printf("\n(已发送至端口:%u)\n",sock_list.MainPort);
}
if (FD_ISSET(sock_list.MainSock, &readfds)) {
//从网络层收到数据
retval = recv(sock_list.MainSock, buf, MAX_BUFFSIZE, 0);
if (retval > 0) {
printf("\n\n---------------------接收栏----------------------\n\n");
printf("收到数据: %s\n\n", buf);
printf("接收时间:%s\n", asctime(timeinfo));
}
}
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
}
如图所示,在网络层,我们将代码也分为三个部分。同样的初始化,端口号设置(这部分有点不同)以及与应用层&物理层间的通信。以下对这三个部分进行一个详细的阐述。
(1)初始化部分与应用层大同小异,值得指出的是初始化的时候应该是用与应用层颠倒的顺序,先输入欲对接的应用层端口号,再设置网络层模块的端口号,否则会带来应用层和网络层软件无法同时使用的尴尬问题。(此部分代码如下)
printf("/---2019年段老师计通网班网元模拟--/\n");
printf("网络层模拟器\n");
//设置应用层端口号,以向应用层发送数据
printf("请输入应用层模块的端口号:");
scanf("%d", &sock_list.MainPort);
//绑定本模块端口号,以接收应用层数据
printf("请设置本模块的端口号:");
scanf("%d", &myPort);
//创建socket
SOCKET socClient = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET是地址协议,代表Ipv4;参数二是套接字类型,SOCK_DGRAM是数据报套接字(udp通信)
if (socClient == SOCKET_ERROR) {
return 0;
}
remote_addr.sin_family = AF_INET;
remote_addr.sin_port = htons(myPort);
remote_addr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
/*
INADDR_ANY是ANY,是绑定地址0.0.0.0上的监听, 能收到任意一块网卡的连接;
INADDR_LOOPBACK, 也就是绑定地址LOOPBAC, 往往是127.0.0.1, 只能收到127.0.0.1上面的连接请求,127.0.0.1即回环地址
*/
while (bind(socClient, (sockaddr*)&remote_addr, sizeof(remote_addr)) != 0) {
printf("本模块的端口号已被占用,请换一个再试:");
scanf("%d", &myPort);
remote_addr.sin_port = htons(myPort);
}
sock_list.MainSock = socClient;
(2)端口号设置除了(1)所述的网络层、应用层端口号设置外,还存在物理层的端口号设置。由于本次设计的模型同时支持多个物理层模块,因此允许用户录入多个物理层模块信息。每次创建一个新的连接就意味着有一个创建的socket被绑定到录入的端口中。创建完成后会尝试向该物理层模块发送信息,等到回应则说明连接成功。之后会将该链接成功的socket加入socket_list下的套接字队列中。这部分代码如下:
//设置物理层接口端口号,以向接口发送数据
printf("请输入接口模块的数量:");
scanf("%d", &portNum);
port = (unsigned short *)malloc(portNum * sizeof(unsigned short));
for (i = 0; i < portNum; i++) {
printf("请输入接口%d的api端口号(从物理层接口模块软件上获取): ", i);
scanf("%d", &(port[i]));
//可以生成多个套接字,每个套接字对应一个接口模块
socClient = socket(AF_INET, SOCK_DGRAM, 0);
if (socClient == SOCKET_ERROR) {
return 0;
}
remote_addr.sin_family = AF_INET;
remote_addr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
remote_addr.sin_port = htons(port[i]);
//向api接口发送一个测试命令
sendto(socClient, "connect", 8, 0, (sockaddr*)&remote_addr, sizeof(remote_addr));
len = sizeof(remote_addr);
retval = recvfrom(socClient, buf, MAX_BUFFSIZE, 0, (sockaddr*)&remote_addr, &len);
if (retval == SOCKET_ERROR) {
printf("接口API关联失败\n");
return 0;
}
//有接收,就可以认为关联成功
printf("接口API连接成功!\n");
//把s放入套接字队列中,今后该套接字在sock_list里的下标可以用于表示接口的编号
insert_list(socClient, port[i], &sock_list);
(3)在通信阶段,同样采用select的非阻塞模式,能同时进行发送和端口监听。值得注意的是编码和转码的过程。当应用层的字符型数据传进网络层时,网络层会将字符型数据进行编码(code),每一个字符形成8 bits(1 byte)的01数据,从而形成能在物理层信道上传输的比特流。同理,当物理层自下而上地往网络层传数据时,网络层会对比特流数据进行解码(decode),每8bits形成一个字符型数据,再传至应用层。网络层充当了应用层和物理层间翻译官的作用。另外,当其中一个物理层接口接收到数据时,网络层会将此数据复制到其它的物理层接口,实现一个类似于广播的作用。
while (1) {
make_fdlist(&sock_list, &readfds);
//本例程采用了基于select机制,不断发送测试数据,和接收测试数据.
selretval = select(0, &readfds, &writefds, &exceptfds, &timeout);
if (selretval == SOCKET_ERROR) {
retval = WSAGetLastError();
break;
}
else if (selretval == 0) {
//定时器触发
continue;
}
if (FD_ISSET(sock_list.MainSock, &readfds)) {
//从应用层收到数据
portLen = sizeof(remote_addr);
len = sizeof(remote_addr);
retval = recvfrom(sock_list.MainSock, buf, MAX_BUFFSIZE, 0, (sockaddr*)&remote_addr, &len);
// retval = recvfrom(sock_list.MainSock,buf,MAX_BUFFSIZE,0,(sockaddr*)&remote_addr,&portLen);
if (retval > 0) {
//将数据编码为"比特流"形式
for (i = 0; i < retval; i++) {
//每次编码8位
code(buf[i], sendbuf + i * 8, 8);
}
//将"比特流"发送到0号接口
remote_addr.sin_port = htons(sock_list.port_array[0]);
sendto(sock_list.sock_array[0], sendbuf, retval * 8, 0, (sockaddr*)&remote_addr, sizeof(remote_addr));
printf("将应用层数据:%s 编码后发送到0号接口\n", buf);
}
}
for (intf = 0; intf < 64; intf++) {
if (sock_list.sock_array[intf] == 0)
continue;
sock = sock_list.sock_array[intf];
if (FD_ISSET(sock, &readfds)) {
//在这个接口上有数据接收,注意接口的编号为i哦。
retval = recv(sock, buf, 4000, 0);
if (retval == 0) {
closesocket(sock);
printf("失去接口%d的关联\n", i);
delete_list(sock, &sock_list);
continue;
}
else if (retval == -1) {
retval = WSAGetLastError();
if (retval == WSAEWOULDBLOCK)
continue;
closesocket(sock);
printf("失去接口%d的关联\n", i);
delete_list(sock, &sock_list);
continue;
}
//收到数据后,打印
printf("%d号接口收到比特流:", intf);
for (i = 0; i < retval; i++) {
linecount++;
if (buf[i] == 0) {
printf("0 ");
}
else {
printf("1 ");
}
if (linecount > 40) {
printf("\n");
linecount = 0;
}
}
printf("\n");
/*
if (intf == 0) {
if (sock_list.sock_array[intf + 1] != 0) {
remote_addr.sin_port = htons(sock_list.port_array[intf + 1]);
printf("从%d号转发到%d号接口\n", intf, intf + 1);
retval = sendto(sock_list.sock_array[1], buf, retval, 0, (sockaddr*)&remote_addr, sizeof(remote_addr));
}
}
*/
//每8位做一个解码
for (i = 0; i < retval; i += 8) {
sendbuf[i / 8] = (char)decode(buf + i, 8);
}
remote_addr.sin_port = htons(sock_list.MainPort);
sendto(sock_list.MainSock, sendbuf, strlen(sendbuf) + 1, 0, (sockaddr*)&remote_addr, sizeof(remote_addr));
printf("从0号口收到的数据上交\n");
}
}
(1)测试步骤
首先在命令控制台下用”ipconfig”命令查看本机网卡ip地址信息
建立物理层间的连接:由于在同一台主机上,因此ip地址保持一致,端口号要相互对应。
当应用层、网络层和物理层的连接准备就绪后,就可以在应用层的界面进行数据发送和接收了。本模型的应用层界面稍微做出了一些优化,界面内的数据用发送栏和接受栏进行区分,以表示数据的流向。并且在数据的下方加上了发送和接收的系统时间以供用户参考。每发送一次数据都会在下方显示传送的端口号以供用户核对。发送栏由用户利用按键调取使用,输入发送数据后即可由先前建立的通路传往另一端的应用层。应用层接收到数据后,程序会自动地将传送过来的数据打在屏幕上。上图展示了整个交互过程。
目前唯一没有解决的问题即是乱码问题,我猜测原因出在了物理层传输的比特流上,如何从时钟流和数据流的混流中提取出用户真正想要的数据是一个尚待解决的问题。这是这个项目进行到下一阶段的时候重点要解决的问题。
2019年3月19日于
电子科技大学信息与通信工程学院网络工程系