Web客户/服务器程序
简介
WWW(World Wide Web)是存储信息的数据库,它遍布世界各地,并通过超链接连接在一起。
Web是互联网上最重要的TCP/IP应用程序
Web是一个分布式的客户/服务器模型,客户使用浏览器访问Web服务器上的资源,它们之间建立一个或多个TCP连接进行通信,Web服务器的知名端口是80
客户浏览信息时与服务器之间交互所使用的应用层协议是超文本传输协议HTTP(Hypertext Transfer Protocol)
web的组成
最初的Web只提供文本文档,现在可以包含多种格式的内容
Web的主要功能组成:
- HTML(HyperText Markup Language)是创建Web页面的标准语言,它是标准通用标记语言SGML(Standard Generalized Markup Language)的具体应用。HTML定义了一些特殊的标记(tag),描述了应该如何显示文档的信息。并定义了超链接指令,指定一个文档如何链接到另一个文档或者Web服务器。
- HTTP(Hypertext Transfer Protocol)是基于TCP/IP的应用层协议,用于处理分布式的超媒体系统,在客户和服务器之间传送HTML和多种不同类型的文档。支持在一个连接上传送多个文件,并且支持鉴权、状态管理(Cookie)、高速缓存等许多高级特征。
- URI(Uniform Resource Identifier)统一资源标识符,对Web中对象的名字和地址进行统一编码,以便容易地引用和访问这些资源。HTTP协议使用URL来定位网络上的资源,完整的URL格式为:http_URL = “http:” “//” host [ “:” port ] [ abs_path [ "?"query ]]
HTML
HTML允许文档的创建者在该文档中包含到其他文档或资源的链接。
HTML把分散在世界各地的文档和资源有效地组织在一起,大大方便了用户的浏览、访问和使用。
HTML是一个只包含ASCII的文本文件,文本和格式指令都是ASCII。接收到HTML文件的计算机按照格式指令的要求显示文件中的文本。它与普通文本的主要区别是:HTML文档是结构化的,整个文件由一系列的元素组成,按照指定的语言规则在逻辑上组织在一起。
元素是用文本标记描述的,标记以符号<开始,紧接元素名,元素名不区分大小写,后跟与这个元素相关的一些属性,属性是可选的,标记以符号>结束。
< 元素 属性1=“值1” 属性2=“值2” ...> 内容 </元素>
HTML文档都在和之间包含着,在中文档又分为两部分:HEAD和BODY。HEAD中是一些描述信息,告诉程序要如何处理这个文档。通常包含文档的标题TITLE,浏览器会把它显示在窗口的顶部。BODY中是文档的实际内容。
HTML常用标记:
HTTP历史
HTTP是HyperText Transfer Protocol的缩写,翻译为“超文本传输协议”。
HTTP/1.0对每次请求-响应,建立并关闭一条连接,定义了完整的消息格式,并规定了在请求和响应中如何使用它们。支持POST方法向服务器提交数据,改变了只能从服务器获取数据的单向操作,从而实现了与服务器的双向交互。
HTTP/1.0的局限性,每次会话都创建一个新的连接,当网页上有多个文档要请求时,频繁地建立和关闭TCP连接,浪费了网络带宽,导致了延迟。HTTP/1.0也缺少缓存、代理机制等。
HTTP1.1新增了很多引人注目的新特性,针对1.0遇到的问题做了重要的改进:
- 默认是持久连接,客户端可以在一个连接上发送多个请求,减少了TCP连接建立和关闭的时间。
- 支持请求部分内容,当请求的文件较大时,客户可以发送多次请求,每次获取文件的一部分内容,这可以方便地实现流媒体,或者传输中断后从文件中断处继续传输
- 支持单主机多个域名,请求中使用的Host可以指定服务器主机,这样一个Web服务器可以有多个域名,而不必每个域名都有独立的IP地址
- 高速缓存,HTTP1.1包含了许多新的机制使缓存工作得更好,使用缓存减少了服务器的负载,并且客户能得到更快的响应,提高了性能
HTTP通信模型
HTTP协议是无状态的,每次会话都独立处理,开始会话时创建一个TCP连接,结束时释放连接。
通信模型非常简单,是基于客户/服务器架构的请求/响应协议。客户与服务器间的交互全部由请求和响应组成
- 请求
由客户端,通常是浏览器软件发起,指定了要从服务器获取的资源(GET方法)或者提交给服务器的资源,如表单数据或上传的文件(POST方法)。请求中的信息包含了协议首部,指明客户端的一些能力信息,如可以处理的媒体类型、支持的语言、编码等。 - 响应
收到请求后,服务器解析这个请求,按照客户端的要求,根据服务器上的资源配置,构造响应,并发送给客户端,在响应中带有状态码,告诉客户端本次操作是否成功。
HTTP是一个基于客户/服务器架构的请求/响应协议,它不维护状态信息,简单,易于管理。基本的通信过程是:客户端发送请求,服务器返回应答。
HTTP消息格式
HTTP为客户端的请求和服务器的响应定义了自己的消息格式
HTTP的消息类型只有两种:请求和响应
符合一个通用的消息结构,以起始行开始,然后是各种消息首部,后跟空行和可选的消息体。
消息首部必须是文本,消息体可以是各种编码数据,HTTP中消息体也被称为实体。
- 请求消息格式
客户端与服务器通信时,它按照用户的要求及本地软件的处理能力,构造出请求消息。请求消息符合通用消息格式。 - 响应消息格式
对每个请求消息,服务器都会发回一个响应消息,把请求的结果告诉客户端。响应消息中通常会带有消息体,是客户端请求的文档或资源
HTTP方法
方法是客户端要求服务器所执行的操作,服务器的行为依赖于请求的对象。
URL是一个文档,服务器返回文件内容;URL为可执行程序时,服务器会执行程序,并返回其执行结果。
方法名称区分大小写,HTTP的方法必须是大写字母。
HTTP/1.1中定义了8个方法,其中GET和HEAD是必须被支持的,其他是可选的
HTTP状态码
对每个请求,服务器都会返回一个响应,响应的第一行是状态行。
状态行包含了支持的协议版本,状态码和原因短语。
状态码可以让机器上的程序自动处理,原因短语是为了方便用户理解。
状态码是由3个数字组成的结果码,第一个数字定义了响应的种类,最后两位没有分类的作用,它们一起提供了更具体信息的说明:
HTTP示例
请求消息中包含了许多首部,大部分都是可选的,只有Host是必须有的
- GET请求,只有消息首部,没有实体
Accept中是客户端能够处理的文档类型
Accept-Language是支持的语言
User-Agent是客户端软件的名称及版本说明
Accept-Encoding是可以处理的编码类型
Connection: Keep-Alive表示客户端希望使用持久连接。
- 响应消息
第一行是状态行HTTP/1.1200 OK, 说明使用的HTTP协议的版本号,状态码200表示这个请求是成功的,OK起说明的作用。
demo
HTTP服务器程序
- 默认的侦听端口是80,可以在命令行中更改为其他的,如果机器上的80端口已经被占用,服务器在bind时会失败
- 是一个循环服务器程序,每次处理一个客户的请求,当有多个客户同时到达时,其他的在队列中等待,当前连接处理完后才能处理其他的。
- 解析请求的命令行,得到客户端请求的文档,确定该文档的内容类型。然后在当前目录中查找是否有对应的文档,如果有就把文档内容发送给客户端;如果客户端没有指定文档名,如“GET / HTTP1.1”,则默认的文档名为“index.html”
- 只支持GET方法,把请求的文档发送给客户端
- .即使客户端发送了通用首部“Connection: Keep-Alive”,也不对其进行解析,因此不支持持久连接,处理完客户的请求就关闭与客户端的连接
- .服务器程序的命令行参数最多只接受一个,即端口号,由用户指定要侦听的端口,如果没有参数,默认在80端口上侦听。
#include <stdio.h> #include <string.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") /* WinSock使用的库函数 */ #define HTTP_DEF_PORT 8090 /* 连接的默认端口 */ #define HTTP_BUF_SIZE 1024 /* 缓冲区的大小 */ #define HTTP_FILENAME_LEN 256 /* 文件名长度 */ struct doc_type { char *suffix; /* 文件后缀 */ char *type; /* Content-Type */ }; struct doc_type file_type[] = { {"html", "text/html" }, {"gif", "image/gif" }, {"jpeg", "image/jpeg" }, { NULL, NULL } // 结束标志 --> 可以不用知道长度信息 }; char *http_res_hdr_tmpl = "HTTP/1.1200 OK\r\nServer: Huiyong's Server <0.1>\r\n" "Accept-Ranges: bytes\r\nContent-Length: %d\r\nConnection: close\r\n" "Content-Type: %s\r\n\r\n"; char *http_get_type_by_suffix(const char *suffix) { struct doc_type *type; for (type = file_type; type->suffix; type++) { if (strcmp(type->suffix, suffix) == 0) return type->type; } return NULL; } //GET / HTTP/1.1 void http_parse_request_cmd(char *buf, int buflen, char *file_name, char *suffix) { int length = 0; char *begin, *end, *bias; begin = strchr(buf, ' '); // 查找' '字符 begin += 1; // --> 指向 / end = strchr(begin, ' '); *end = 0; // 字符串结束标志 --> 使用string.h中的函数,end后面的内容都会被忽视 // 此时 begin到end的内容为 // http://jsuacm.cn/ bias = strrchr(begin, '/'); length = end - bias; if ((*bias == '/') || (*bias == '\\')) { bias++; // 去掉/ length = 0; } /* 得到文件名 */ if (length > 0) { memcpy(file_name, bias, length); file_name[length] = 0; // 字符串结束标志 begin = strchr(file_name, '.'); if (begin) strcpy(suffix, begin + 1); } } int http_send_response(SOCKET soc, char *buf, int buf_len) { int read_len, file_len, hdr_len, send_len; char *type; char read_buf[HTTP_BUF_SIZE]; char http_header[HTTP_BUF_SIZE]; char file_name[HTTP_FILENAME_LEN] = "index.html", suffix[16] = "html"; FILE *res_file; /* 得到文件名和后缀 */ http_parse_request_cmd(buf, buf_len, file_name, suffix); res_file = fopen(file_name, "rb+"); /* 用二进制格式打开文件 */ //以二进制模式打开文件,这样才能得到文件的准确长度,如果用“r+”打开, //默认为文本方式,文件中的回车换行,不同的系统有不同的处理, //计算文件长度时会把换行去掉,用fseek得到文件大小时,长度会不正确。 if (res_file == NULL) { printf("[Web] The file [%s] is not existed\n", file_name); return 0; } fseek(res_file, 0, SEEK_END); file_len = ftell(res_file); fseek(res_file, 0, SEEK_SET); type = http_get_type_by_suffix(suffix); /*文件对应的Content-Type */ if (type == NULL) { printf("[Web] There is not the related content type\n"); return 0; } /* 构造HTTP首部,并发送 */ hdr_len = sprintf(http_header, http_res_hdr_tmpl, file_len, type); send_len = send(soc, http_header, hdr_len, 0); if (send_len == SOCKET_ERROR) { fclose(res_file); printf("[Web] Fail to send, error = %d\n", WSAGetLastError()); return 0; } do /* 发送文件,HTTP的消息体 */ { read_len = fread(read_buf, sizeof(char), HTTP_BUF_SIZE, res_file); if (read_len > 0) { send_len = send(soc, read_buf, read_len, 0); file_len -= read_len; } } while ((read_len > 0) && (file_len > 0)); fclose(res_file); return 1; } int main(int argc, char **argv) { WSADATA wsa_data; SOCKET srv_soc = 0, acpt_soc; /* socket句柄 */ struct sockaddr_in serv_addr; /* 服务器地址 */ struct sockaddr_in from_addr; /* 客户端地址 */ char recv_buf[HTTP_BUF_SIZE]; unsigned short port = HTTP_DEF_PORT; int from_len = sizeof(from_addr); int result = 0, recv_len; if (argc == 2) /* 端口号 */ port = atoi(argv[1]); WSAStartup(MAKEWORD(2,0), &wsa_data); /* 初始化WinSock资源 */ srv_soc = socket(AF_INET, SOCK_STREAM, 0); /* 创建socket */ if (srv_soc == INVALID_SOCKET) { printf("[Web] socket() Fails, error = %d\n", WSAGetLastError()); return -1; } /* 服务器地址 */ serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); result = bind(srv_soc, (struct sockaddr *) &serv_addr, sizeof(serv_addr)); if (result == SOCKET_ERROR) /* 绑定失败 */ { closesocket(srv_soc); printf("[Web] Fail to bind, error = %d\n", WSAGetLastError()); return -1; } result = listen(srv_soc, SOMAXCONN); printf("[Web] The server is running ... ...\n"); while (1) { acpt_soc = accept(srv_soc, (struct sockaddr *)&from_addr, &from_len); if (acpt_soc == INVALID_SOCKET) /* 接受失败 */ { printf("[Web] Fail to accept, error = %d\n", WSAGetLastError()); break; } printf("[Web] Accepted address:[%s], port:[%d]\n", inet_ntoa(from_addr.sin_addr), ntohs(from_addr.sin_port)); recv_len = recv(acpt_soc, recv_buf, HTTP_BUF_SIZE, 0); if (recv_len == SOCKET_ERROR) /* 接收失败 */ { closesocket(acpt_soc); printf("[Web] Fail to recv, error = %d\n", WSAGetLastError()); break; } recv_buf[recv_len] = 0; /* 向客户端发送响应数据 */ result = http_send_response(acpt_soc, recv_buf, recv_len); closesocket(acpt_soc); } closesocket(srv_soc); WSACleanup(); printf("[Web] The server is stopped.\n"); return 0; }
HTTP客户端程序
只实现了GET方法,向用户输入的URL发送请求,把接收到的数据保存在文件中,同时显示在屏幕上。
#include <stdio.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") /* WinSock使用的库函数 */ #define HTTP_DEF_PORT 80 /* 连接的默认端口 */ #define HTTP_BUF_SIZE 1024 /* 缓冲区的大小 */ #define HTTP_HOST_LEN 256 /* 主机名长度 */ char *http_req_hdr_tmpl = "GET %s HTTP/1.1\r\n" "Accept: image/gif, image/jpeg, */*\r\nAccept-Language: zh-cn\r\n" "Accept-Encoding: gzip, deflate\r\nHost: %s:%d\r\n" "User-Agent: Huiyong's Browser <0.1>\r\nConnection: Keep-Alive\r\n\r\n"; // http://www.baidu.com:8080/index.html // 或者 www.baidu.com:8080/index.html void http_parse_request_url(char *buf, char *host, unsigned short *port, char *file_name) { int length = 0; char port_buf[8]; char *buf_end = (char *)(buf + strlen(buf)); char *begin, *host_end, *colon, *file; /* 查找主机的开始位置 */ begin = strstr(buf, "//"); // 有http:// 和没有http:// 两种情况 begin = (begin ? begin + 2 : buf); // 端口号 colon = strchr(begin, ':'); host_end = strchr(begin, '/'); if (host_end == NULL) { host_end = buf_end; }else { /* 得到文件名 */ file = strrchr(host_end, '/'); if (file && (file + 1) != buf_end) strcpy(file_name, file + 1); } if (colon) /* 得到端口号 */ { colon++; length = host_end - colon; memcpy(port_buf, colon, length); port_buf[length] = 0; *port = atoi(port_buf); host_end = colon -1; } /* 得到主机信息 */ length = host_end - begin; memcpy(host, begin, length); host[length] = 0; } int main(int argc, char **argv) { WSADATA wsa_data; SOCKET http_sock = 0; /* socket句柄 */ struct sockaddr_in serv_addr; /* 服务器地址 */ struct hostent *host_ent; int result = 0, send_len; char data_buf[HTTP_BUF_SIZE]; char host[HTTP_HOST_LEN] = "127.0.0.1"; unsigned short port = HTTP_DEF_PORT; unsigned long addr; char file_name[HTTP_HOST_LEN] = "index.html"; FILE *file_web; if (argc != 2) { printf("[Web] input : %s http://www.test.com[:80]/index.html", argv[0]); return -1; } http_parse_request_url(argv[1], host, &port, file_name); WSAStartup(MAKEWORD(2,0), &wsa_data); /* 初始化WinSock资源 */ addr = inet_addr(host); if (addr == INADDR_NONE) { host_ent = gethostbyname(host); if (!host_ent) { printf("[Web] invalid host\n"); return -1; } memcpy(&addr, host_ent->h_addr_list[0], host_ent->h_length); } /* 服务器地址 */ serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); serv_addr.sin_addr.s_addr = addr; http_sock = socket(AF_INET, SOCK_STREAM, 0); result=connect(http_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); if (result == SOCKET_ERROR) /* 连接失败 */ { closesocket(http_sock); printf("[Web] fail to connect, error = %d\n", WSAGetLastError()); return -1; } /* 发送HTTP请求 */ send_len = sprintf(data_buf, http_req_hdr_tmpl, argv[1], host, port); result = send(http_sock, data_buf, send_len, 0); if (result == SOCKET_ERROR) /* 发送失败 */ { printf("[Web] fail to send, error = %d\n", WSAGetLastError()); return -1; } file_web = fopen(file_name, "w+"); do /* 接收响应并保存到文件中 */ { result = recv(http_sock, data_buf, HTTP_BUF_SIZE, 0); if (result > 0) { fwrite(data_buf, 1, result, file_web); /* 在屏幕上输出 */ data_buf[result] = 0; printf("%s", data_buf); } } while(result > 0); fclose(file_web); closesocket(http_sock); WSACleanup(); return 0; }