CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~
实验题目
Echo 实验
实验目的
掌握套节字的基本使用方法
参考资料
- https://www.cnblogs.com/hgwang/p/6074038.html (套接字)
- https://www.jb51.net/article/37410.htm (字符串)
- https://docs.microsoft.com/en-us/c/c-runtime-library/stream-i-o?view=vs-2017 (字符串)
- https://docs.microsoft.com/en-us/c/c-runtime-library/reference/crt-alphabetical-function-reference?view=vs-2017#s (字符串)
- http://www.runoob.com/cprogramming/ (字符串)
实验环境
- Windows + VS 2012 http://172.18.187.9/netdisk/default.aspx?vm=17net
- 对于 VS2015 和 VS2017 默认使用安全周期检查,如果不关闭 VS 的安全周期检查,很多字符串函数都不能用。
- Linux + gcc
这里我使用的环境是 Windows 10 + VSCode + gcc version 8.1.0 (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project)
实验内容
编写 TCP Echo 增强程序
实验要求
服务器把客户端发送来的任何消息都返回给客户端,返回的消息前面要加上服务器的当前时间。客户端把返回的消息显示出来。客户端每输入一条消息就建立 TCP 连接,并把消息发送给服务器,在收到服务器回应后关闭连接。在这一基础上,服务器在收到客户端的消息时显示服务器的当前时间、客户端的 IP 地址、客户端的端口号和客户端发来的信息,并把它们一并返回给客户端。客户端在发送消息后把服务器发回给它的消息显示出来。要求服务器直接从 accept()的参数 fsin 中得到客户端的 IP 地址和端口号。服务器获取 IP 地址后要求直接使用 s_un_b 的四个分量得到 IP 地址,不能使用函数 inet_ntoa()转换 IP 地址。
只运行客户端程序而不运行服务器程序会出现什么错误,截屏并说明原因
如图,退出了服务器程序后再运行客户端程序,得到了 10057 的报错。
服务器如何可以退出循环
kbhit()在执行时,检测是否有按键按下,有按下返回非 0 值,没有按下则返回 0,是非阻塞函数;因此,预期在监听的过程中将服务器程序从后台激活并按下键盘按键即可退出循环。
然而,在实际运行中这样是失效的,正是因为他是非阻塞函数,而下面收发信息却是阻塞的。因此退出循环只有我们在命令行中手动按 Ctrl+C 了。
截屏(ctrl+alt+PrintScreen)服务器和客户端的运行结果(注明客户端和服务器)
客户端: 服务器:
服务器的全部源代码(或自选主要代码)
#include <stdio.h>
#include <time.h>
#include <winsock2.h>
#include <conio.h>
#define BUFLEN 2000
#define WSVERS MAKEWORD(2, 0)
#pragma comment(lib, "ws2_32.lib") //使用winsock 2.2 library
int main(int argc, char *argv[]) // argc: 命令行参数个数,例如:C:\> TCPServer 8080 argc=2 argv[0]=「TCPServer", argv[1]="8080"
{
struct sockaddr_in fsin; // the from address of a client
SOCKET msock, ssock; // master & slave sockets
WSADATA wsadata;
char service[] = "50500";
struct sockaddr_in sin; // an Internet endpoint address
int alen; // from-address length
char pts[BUFLEN + 1]; // pointer to time string
time_t now; // current time
WSAStartup(WSVERS, &wsadata); // 加载winsock library,WSVERS为请求版本,wsadata返回系统实际支持的最高版本
msock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字。 参数:因特网协议簇(family),字节流,TCP协议号。 返回:要监听套接字的描述符或INVALID_SOCKET
memset(&sin, 0, sizeof(sin)); //从&sin开始的长度为sizeof(sin)的内存清0 , sin为一个地址结构
sin.sin_family = AF_INET; //因特网地址簇(INET-Internet)
sin.sin_addr.s_addr = INADDR_ANY; //监听所有(接口的)IP地址(32位),// 0.0.0.0
sin.sin_port = htons((u_short)atoi(service)); //监听的端口号(16位) 。atoi--把ascii转化为int,htons—主机序到网络序
bind(msock, (struct sockaddr *)&sin, sizeof(sin)); // 通过sin把要监听的IP地址和端口号绑定到套接字上
listen(msock, 5); // 建立长度为5的连接请求队列,并开始监听是否有连接请求到来,来了则放入队列
printf("Server Start to listen.\n");
while (!_kbhit()) // 检测是否有按键 (什么时候执行?)
{
alen = sizeof(struct sockaddr);
ssock = accept(msock, (struct sockaddr *)&fsin, &alen); // accept:如果有新的连接请求,返回连接套接字,否则,被阻塞,fsin包含客户端IP地址和端口号
time(&now); // 取得系统时间
int cnt = sprintf(pts, "Accept Time:\n"
"%s"
"sin_port:\n"
"%u\n"
"sin_addr:\n"
"%d.%d.%d.%d\n"
"Receive Message:\n",
ctime(&now),
fsin.sin_port,
fsin.sin_addr.S_un.S_un_b.s_b1,
fsin.sin_addr.S_un.S_un_b.s_b2,
fsin.sin_addr.S_un.S_un_b.s_b3,
fsin.sin_addr.S_un.S_un_b.s_b4); // 把时间转换为字符串
int cc = recv(ssock, pts + cnt, BUFLEN - cnt, 0);
pts[cnt + cc] = 0;
printf("\n%s\n", pts);
send(ssock, pts, cnt + cc, 0);
closesocket(ssock); // 关闭连接套接字
}
closesocket(msock);
WSACleanup();
system("pause");
}
客户端的全部源代码(或自选主要代码)
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#define BUFLEN 2000
#define WSVERS MAKEWORD(2, 0)
#pragma comment(lib, "ws2_32.lib")
int main(int argc, char *argv[])
{
char host[] = "127.0.0.1"; // server IP to connect,127.0.0.1指本机
char service[] = "50500"; // server port to connect
struct sockaddr_in sin; // an Internet endpoint address
char buf[BUFLEN + 1], msg[BUFLEN + 1]; // buffer for one line of text
SOCKET sock; // socket descriptor
int cc; // recv character count
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata);
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
memset(&sin, 0, sizeof(sin)); // sin的内存清0
sin.sin_family = AF_INET; // 因特网地址簇
sin.sin_addr.s_addr = inet_addr(host); // 服务器IP地址(32位)
sin.sin_port = htons((u_short)atoi(service)); // 服务器端口号(16位)
printf("Send the Message:\n");
gets(msg);
int ret = connect(sock, (struct sockaddr *)&sin, sizeof(sin)); // 连接到服务器.无错时,返回0, // 否则,返回SOCKET_ERROR ,可以调用 // 函数WSAGetLastError取得错误代码
cc = send(sock, msg, strlen(msg), 0); //把缓冲区pts的数据发送出去,len为要发送的字节数, // 返回值:(>0) 实际发送的字节数(≤len), (=0) 对方正常关闭, // (=SOCKET_ERROR) 出错,用函数WSAGetLastError取错误码。
cc = recv(sock, buf, BUFLEN, 0); // BUFLEN为缓冲区buf的长度。 // 返回值:接收的字符数(>0)、对方已关闭(=0) 或连接出错(<0)
if (cc == SOCKET_ERROR)
printf("Error:\n%d.\n", GetLastError());
else if (cc == 0)
printf("Server closed.\n");
else if (cc > 0)
{
buf[cc] = '\0'; // ensure null-termination
printf("\n%s\n", buf); // 显示所接收的字符串
}
closesocket(sock); // 关闭套接字
WSACleanup(); // 卸载winsock library
system("pause");
}
编写 UDP Echo 增强程序
实验要求
修改 UDP 例程,完成 Echo 功能,即当客户端发来消息时,服务器显示出服务器的当前时间、客户端的 IP、客户端的端口号和客户发来的信息,并把它们一并发回给客户端,客户端然后把它们显示出来。 服务器可以直接从 recvfrom()的参数 from 中得到客户端的 IP 地址和端口号,并且服务器用 sendto()发回给客户端消息时可以直接用该参数 from 作为参数 toAddr。可以使用 inet_ntoa()转换客户端 IP 地址。 客户端程序的 recvfrom()可以直接使用原来 sendto 使用的 sock。该 sock 已经绑定了客户端的 IP 地址和端口号,客户端可以直接用来接收数据。
只运行客户端程序而不运行服务器程序会出现什么错误,截屏并说明原因
如图,在发送时没有报错,但是试图从服务器获得信息时获得了 10054 的报错。
截屏服务器和客户端的运行结果(注明客户端和服务器)
服务器:
服务器的全部源代码(或自选主要代码)
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <conio.h>
#include <winsock2.h>
#define BUFLEN 2000 // 缓冲区大小
#define WSVERS MAKEWORD(2, 2) // 指明版本2.2
#pragma comment(lib, "ws2_32.lib") // 加载winsock 2.2 Llibrary
void main(int argc, char *argv[])
{
char host[] = "127.0.0.1"; // server IP Address to connect
char service[] = "50500"; // server port to connect
struct sockaddr_in sin; // an Internet endpoint address
struct sockaddr_in from; // sender address
int fromsize = sizeof(from);
char buf[BUFLEN + 1], pts[BUFLEN + 1]; // buffer for one line of text
SOCKET sock; // socket descriptor
int cc; // recv character count
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata); // 加载winsock library,WSVERS为请求版本,wsadata返回系统实际支持的最高版本。
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 创建UDP套接字, 参数:因特网协议簇(family),数据报套接字,UDP协议号, 返回:要监听套接字的描述符或INVALID_SOCKET
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY; // 绑定(监听)所有的接口。
sin.sin_port = htons((u_short)atoi(service)); // 绑定指定接口。atoi--把ascii转化为int,htons -- 主机序(host)转化为网络序(network), 为short类型。
bind(sock, (struct sockaddr *)&sin, sizeof(sin)); // 绑定本地端口号(和本地IP地址)
printf("UDPServer Start.\n");
while (!_kbhit())
{ //检测是否有按键
cc = recvfrom(sock, buf, BUFLEN, 0, (SOCKADDR *)&from, &fromsize); //接收客户数据。返回结果:cc为接收的字符数,from中将包含客户IP地址和端口号。
if (cc == SOCKET_ERROR)
{
printf("recvfrom() failed; %d\n", WSAGetLastError());
break;
}
else if (cc == 0)
break;
else
{
buf[cc] = 0;
time_t now;
time(&now); // 取得系统时间
sprintf(pts,
"Accept Time:\n"
"%s"
"sin_port:\n"
"%u\n"
"sin_addr:\n"
"%d.%d.%d.%d\n"
"Receive Message:\n"
"%s\n",
ctime(&now),
from.sin_port,
from.sin_addr.S_un.S_un_b.s_b1,
from.sin_addr.S_un.S_un_b.s_b2,
from.sin_addr.S_un.S_un_b.s_b3,
from.sin_addr.S_un.S_un_b.s_b4,
buf);
printf("%s", pts);
cc = sendto(sock, pts, strlen(pts), 0, (SOCKADDR *)&from, fromsize);
}
}
closesocket(sock);
WSACleanup(); // 卸载某版本的DLL
system("pause");
}
客户端的全部源代码(或自选主要代码)
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#define BUFLEN 2000 // 缓冲区大小
#define WSVERS MAKEWORD(2, 2) // 指明版本2.2
#pragma comment(lib, "ws2_32.lib") // 加载winsock 2.2 Llibrary
void main(int argc, char *argv[])
{
char host[] = "127.0.0.1"; // server IP to connect
char service[] = "50500"; // server port to connect
struct sockaddr_in toAddr, from; // an Internet endpoint address
int fromsize = sizeof(from);
char buf[BUFLEN + 1]; // buffer for one line of text
SOCKET sock; // socket descriptor
int cc; // recv character count
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata); // 启动某版本Socket的DLL
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
memset(&toAddr, 0, sizeof(toAddr));
toAddr.sin_family = AF_INET;
toAddr.sin_port = htons((u_short)atoi(service)); //atoi:把ascii转化为int. htons:主机序(host)转化为网络序(network), s--short
toAddr.sin_addr.s_addr = inet_addr(host); //如果host为域名,需要先用函数gethostbyname把域名转化为IP地址
printf("Send the Message:\n");
gets(buf);
cc = sendto(sock, buf, strlen(buf), 0, (SOCKADDR *)&toAddr, sizeof(toAddr));
if (cc == SOCKET_ERROR)
printf("Error:\n%d\n", WSAGetLastError());
else
{
cc = recvfrom(sock, buf, BUFLEN, 0, (SOCKADDR *)&from, &fromsize);
if (cc == SOCKET_ERROR)
printf("Receive Error:\n%d\n", WSAGetLastError());
else
printf("%s", buf);
}
closesocket(sock);
WSACleanup(); // 卸载某版本的DLL
system("pause");
}
实验体会
在 Windows 下使用 gcc 编译,需要加编译指令-lwsock32 来引入套接字的库,于是写了如下脚本帮助编译:
gcc TCPServer.c -o TCPServer -lwsock32
gcc TCPClient.c -o TCPClient -lwsock32
节省了不少时间。
服务器对收到的消息进行处理这里使用了 sprintf 这个函数,感觉相比别的方法简化了很多代码。
想当然的认为服务器程序只需要按下键盘就会退出,说明一开始对阻塞的理解还不透彻啊。