目录
一、设计目标
完成基础的tcp连接,支持基础的client与其连接使用fork()来支持并发访问服务器简单的http访问,支持静态页面访问,需要一定的报错机制,如404页面的建立。
- socket实现简单Http服务器,完成html的解析;
- 运行该服务器可以通过浏览器访问服务器目录下的 Html文件、jpg图片、css文件的载入,完成初步的Http服务器功能。
二、相关技术
Server端:
完成socket(),bind(),listen()这些初始化工作后,调用accept()方法阻塞等待(其实就是进入一个死循环),等待CLient的connect()方法连接 Client端;
先调用socket(),然后调用connect()想要与Server端进行连接,这个时候就会进行传说中的TCP三次握手,也就是在Client 发起connect(),并且Server进入accept()阻塞等待时发生三次握手。
图1 三次握手
Client:浏览器
三、设计内容
3.1需求分析
Web服务器使用HTTP协议与客户端(即浏览器)通信,而HTTP协议又基于TCP/IP协议。浏览器输入地址后,首先和web服务器建立tcp连接,然后浏览器发送http请求报文, web服务器响应处理这个报文,再给他回复一个响应,然后服务器主动断开连接。实现服务器与客户端间的通信。可以实现HTTP请求中的GET方法。
还可供静态网页浏览功能,如可浏览:HTML页面,无格式文本,常见图像格式等,还可以检查一些明显错误报告给客户端,如:403无权访问,404找不到所请求的文件,501不支持相应方法等。在服务器端可输出HTTP响应的相关信息。服务器端可配置参数,如:主目录,首页文件名,HTTP端口号等项。
3.2概要设计
图2 TCP流程图 代码概要:
(1)加载协议栈
(2)创建监听套接字,用于监听客户请求
(3)创建服务器地址:IP+端口号
(4)绑定监听套接字和服务器地址
(5)通过监听套接字进行监听
(6)接受客户端的连接请求,返回与该客户建立的连接套接字
(7)创建线程接受浏览器请求
3.3详细设计(主要函数分析)
3.3.1(socket)
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议。
socket()函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。WSADATA wsdata; //先确定socket版本信息,WSADATA是一种结构体,存放windows socket初始化信息 int isok = WSAStartup(MAKEWORD(2, 2), &wsdata); //WSAStartup为异步套接字启动函数,用来指定版本号及获取特定的细节 第一个参数:需要版本号,MAKEWORD是制造一个short类型,高字节表示小版本号,低字节表示主版本号,第二个参数:传出参数,用来获取信息。 SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个socket(定义一个SOCKET类型的名为server的变量),第一个参数:协议族,决定了socket的地址类型(AF_INET表示用Ipv4),第二个参数:传输类型,SOCK_STREAM表示流传输,第三个参数:指定传输协议,IPPROTO_TCP表示使用tcp协议。
3.3.2(bind)
将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()或listen()调用前使用。当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
//初始化协议地址,绑定ip和端口号 struct sockaddr_in seraddr; //自定义sockaddr_in结构体,定义服务端 seraddr.sin_family = AF_INET; //sin_family指代协议族,为socket函数第一个参数AF_INET(Ipv4) seraddr.sin_port = htons(80); //sin_port存储端口号,80端口服务于HTTP,网络中的数据和电脑上的数据存储是有区别的,必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字 seraddr.sin_addr.s_addr = (INADDR_ANY); //sin_addr存储IP地址,INADDR_ANY表示可监听任意绑定了80端口的ip地址 isok = bind(server, (sockaddr*)&seraddr, sizeof(seraddr)); //bind()为绑定函数,将一本地地址与一套接口捆绑,sizeof用于求字节大小
3.3.3(listen)
创建一个套接口并监听申请的连接。为了接受连接,先用socket()创建一个套接口的描述字,然后用listen()创建套接口并为申请进入的连接建立一个后备日志,然后便可用accept()接受连接了。在网络通信中, 客户端通常处于主动的一方, 而服务器则是被动的一方, 服务器是被连接的, 所以他要时刻准备着被连接, 所以就需要调用 listen() 来监听, 等着被连接。
//通过监听套接字进行监听 if (listen(sListen, 5) == SOCKET_ERROR) //第一个参数,监听者,第二个参数,连接请求队列的最大长度
3.3.4(accept)
accept()是在一个套接口接受的一个连接。accept()系统调用主要用在基于连接的套接字类型,它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
//接受客户端的连接请求,返回与该客户建立的连接套接字 SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //sockfd:套接字描述符,该套接口在listen()后监听连接。addr:指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。addrlen:指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
3.3.5(connect)
connect()用于建立与指定socket的连接。将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen); //sockfd:标识一个套接字。serv_addr:套接字s想要连接的主机地址和端口号。addrlen:name缓冲区的长度。
四、完整代码
#include<stdio.h>
#include<WinSock2.h> //socket通信,系统头文件,包含网络头文件,引入静态库
#include<WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib") //链接Ws2_32.lib这个库,加载了连接库文件,实现通信程序的管理
//constexpr auto SERADDR = "192.168.3.11";
void SendHtml(SOCKET s, char* filename); //先声明该函数
int merror(int redata, int error, char* showinfo) //创建merror函数提供报错机制,后续多次使用便于判断是否失败,
{ //三个参数依次为返回值,比较值(错误代号),打印语句
if (redata == error)
{
perror(showinfo); //perror作用为将上一个函数发生错误的原因输出
printf("\n");
getchar(); //缓冲作用,使程序不会立刻退出
return -1;
}
return 1;
}
int main()
{
printf("计算机网络课程设计:C语言实现简易Web服务器...\r\n");
WSADATA wsdata; //1,确定socket版本信息,WSADATA是一种结构体,存放windows socket初始化信息
int isok = WSAStartup(MAKEWORD(2, 2), &wsdata); //WSAStartup为异步套接字启动函数,用来指定版本号及获取特定的细节
//第一个参数:需要版本号,MAKEWORD是制造一个short类型,高字节表示小版本号,低字节表示主版本号
//第二个参数:传出参数,用来获取信息
merror(isok, WSAEINVAL, "socket请求失败...\n"); //WSAEINVAL为相应的错误代号
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //2,创建一个socket(定义一个SOCKET类型的名为server的变量)
//第一个参数:协议族,决定了socket的地址类型(AF_INET表示用Ipv4)
//第二个参数:传输类型,SOCK_STREAM表示流传输
//第三个参数:指定传输协议,IPPROTO_TCP表示使用tcp协议
merror(server, INVALID_SOCKET, "socket创建失败...\n"); //INVALID_SOCKET为相应的错误代码
//3.初始化协议地址,绑定ip和端口号
struct sockaddr_in seraddr; //自定义sockaddr_in结构体,定义服务端
seraddr.sin_family = AF_INET; //sin_family指代协议族,为socket函数第一个参数AF_INET(Ipv4)
seraddr.sin_port = htons(80); //sin_port存储端口号,80端口服务于HTTP,网络中的数据和电脑上的数据存储是有区别的,
//seraddr.sin_addr.s_addr = inet_addr(SERADDR); //必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字
seraddr.sin_addr.s_addr = (INADDR_ANY); //sin_addr存储IP地址
//INADDR_ANY表示可监听任意绑定了80端口的ip地址
isok = bind(server, (sockaddr*)&seraddr, sizeof(seraddr)); //bind()为绑定函数,将一本地地址与一套接口捆绑,sizeof用于求字节大小
merror(isok, SOCKET_ERROR, "bind绑定信息失败...\n"); //SOCKET_ERROR为相应的错误代号
isok = listen(server, 5); //4,listen()创建一个套接口并监听申请的连接,监听客服端,
//第一个参数,监听者,第二个参数,连接请求队列的最大长度
merror(isok, SOCKET_ERROR, "listen监听失败...\n");
struct sockaddr_in claddr; //自定义sockaddr_in结构体,定义客户端
int cllen = sizeof(claddr); //求claddr字节数
while (1) //while循环目的:使accept一直等待并接受直至到达连接请求队列的最大长度
{
printf("正在等待连接中...\n");
SOCKET client = accept(server, (sockaddr*)&claddr, &cllen); //第一个参数表示谁接受连接
//第二个参数表示是谁连接进来了
//第三个参数表示用来保存信息的结构体的大小
//返回值为链接进来的客户端的socket
merror(client, INVALID_SOCKET, "连接失败...\n");
printf("连接成功...\n");
char recvdata[1024] = ""; //recvdata表示接收到的数据,初始化为空
recv(client, recvdata, sizeof(recvdata), 0); //保存客户端发送的数据
//第一个参数:接受从哪来的消息 第二个参数:接受消息的指针,
//第三个参数:接受消息的指针内存大小(此处给1024)
//第四个参数:0表示默认的收发方式,一次都收完,等待流传输完成之后一次收取
printf("%s 共收到%d字节数据~\n\n", recvdata, strlen(recvdata));
//char senddata[1024] = "<h1 style=\"color:blue;\">文字描述</h1>";
//send(client, senddata, strlen(senddata), 0);
char* filename = "./index.html";
SendHtml(client, filename); //第一个参数:发送对象,第二个参数:文件路径
closesocket(client); //关闭连接
}
closesocket(server); //关闭服务器socket
WSACleanup(); //关闭套接字请求
while (1);
return 0;
}
void SendHtml(SOCKET s, char* filename) //发送整个htnl网页,第一个参数:发送对象,第二个参数:文件路径
{
FILE* pfile = fopen(filename, "rb"); //r表示只读
if (pfile == NULL)
{
printf("文件打开失败");
return;
}
char tempdata[1024] = "";
do
{
//fgets(tempdata, 1024, pfile); //pfile文件指针
long rlen = fread(tempdata, 1, sizeof(tempdata), pfile);
//send(s, tempdata, rlen, 0);
send(s, tempdata, strlen(tempdata), 0);
} while (!feof(pfile)); //循环语句确保文件全部读取
}
代码参考自“顽石老师”

