3.基于并发的TCP客户机/服务器模型
1)TCP协议的基本特征
A.面向连接:参与通信的双发在正式通信之前需要先建立连接,以形成一条虚拟电路,所有的后续通信都在这条虚电路上完成。类似于电话通信业务。正式通话之前要先拨号,拨通了才能讲话。拨号的过程就是一个建立连接的过程。
三次握手:
客户机 服务器
发起连接请求 --------SYN(n)--------> 被动侦听
等待应答 <-ACK(n+1)+SYN(m)- 可以接受
反向确认 ------ACK(m+1)------> 连接成功
一旦三路握手完成,客户机和服务器的网络协议栈中就会保存有关连接的信息,此后的通信内容全部基于此连接实现数据传输。通信过程中任何原因导致的连接中断,都无法再继续通信,除非重新建立连接。
B.可靠传输:超时重传。每次发送一个数据包,对方都需要在一个给定的时间窗口内予以应答,如果超过时间没有收到对方的应答,发送方就会重发该数据包,只有重试过足够多的次数依然失败才会最终放弃。
C.保证顺序:发送端为每一个数据包编制序列号,接收端会根据序列号对所接收到的数据包进行重排,避免重复和乱序。
D.流量控制:协议栈底层在从另一端接收数据时,会不断告知对方它能够接收多少字节的数据,即所谓通告窗口。任何时候,这个窗口都反映了接收缓冲区可用空间的大小,从而确保不会因为发送方发送数据过快或过慢导致接收缓冲区出现上溢出或下溢出。
E.流式传输:以字节流形式传输数据,数据包在传输过程中没有记录边界。应用程序需要根据自己的规则来划分出数据包的记录边界。
a)定长记录
b)不定长记录加分隔符
c)定长长度加不定长记录
F.全双工:在给定的连接上,应用程序在任何时候都既可以发送数据也可以接收数据。因此TCP必须跟踪每个方向上数据流的状态信息,如序列号和通告窗口大小等。
2)TCP连接的生命周期
被动打开:通过侦听套接字感知其它主机发起的连接请求。
三路握手:TCP连接的建立过程。
| TCP包头 | TCP包体 |
包头部分总共20字节,含有6个标志位:SYN/ACK/FIN/RST/...,发送序列号和应答序列号...
数据传输:超时重传、流量控制、面向字节流、全双工
四次挥手:
客户机 服务器
主动关闭 ---------FIN(n)-------> 被动关闭
等待应答 <-----ACK(n+1)------ 关闭应答
确定关闭 <--------FIN(m)------- 已经关闭
关闭应答 ------ACK(m+1)-----> 等待应答
3)常用函数
在指定套接字上启动对连接请求的侦听,即将该套接字置为被动模式,因为套接字都缺省为主动模式。
int listen(int sockfd, int backlog);
成功返回0,失败返回-1。
sockfd - 套接字描述符
backlog - 未决连接请求队列的最大长度
在指定的侦听套接字上等待并接受连接请求
int accept(int sockfd, struct sockaddr* addr,size_t* addrlen);
成功返回连接套接字描述符用于后续通信,失败返回-1。
sockfd - 侦听套接字描述符
addr - 输出连接请求发起者的地址信息
addrlen - 输入输出连接请求发起者地址信息的字节数
该函数由TCP服务器调用,返回排在已决连接队列首部的连接套接字对象的描述符,若已决连接队列为空,该函数会阻塞。
^^^^^^^^
非并发的TCP服务器
创建套接字(socket)
绑定地址(bind)
启动侦听(listen)
等待连接(accept)<--+
接收请求(read)<-+ |
业务处理(...) | |
发送响应(write)--+--+
并发的TCP服务器
创建套接字(socket)
绑定地址(bind)
启动侦听(listen)
等待连接(accept)<---+
|
产生客户子进程(fork)-+
接收请求(read)<-+
业务处理(...) |
发送响应(write)--+
接收数据
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
返回0表示客户机或服务器已关闭,失败返回-1
flags - 接收标志,取0等价于read
MSG_DONTWAIT: 非阻塞接收。对于阻塞模式,当接收缓冲区为空时,该函数会阻塞,直到接收缓冲区不空为止。如果使用了此标志位,当接收缓冲区为空时,该函数会返回-1,并置errno为EAGAIN或EWOULDBLOCK。
MSG_OOB: 接收带外数据。
MSG_PEEK: 瞄一眼数据,只将接收缓冲区中的数据复制到buf缓冲区中,但并不将其从接收缓冲区中删除。
MSG_WAITALL: 接收到所有期望接收的数据才返回,如果接收缓冲区中的数据不到 len 个字节,该函数会阻塞,直到可接收到len个字节为止。
发送数据
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
flags - 接收标志,取0等价于write
MSG_DONTWAIT: 非阻塞发送。对于阻塞模式,当发送缓冲区的空余空间不足以容纳期望发送的字节数时,该函数会阻塞,直到发送缓冲区的空余空间足以容纳期望发送的字节数为止。如果使用了此标志位,能发送多少字节就发送多少字节,不会阻塞,甚至可能返回0表示发送缓冲区满,无法发送数据。
MSG_OOB: 发送带外数据。
MSG_DONTROUT: 不查路由表,直接在本地网中寻找目的主机。
代码:tcpsvr.c、tcpcli.c
/* tcpsvr.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void sigchld(int signum) {
for (;;) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid == -1) {
if (errno != ECHILD) {
perror("waitpid");
exit(-1);
}
printf("服务器:全部子进程都已退出\n");
break;
}
if (pid)
printf("服务器:发现%d子进程退出\n", pid);
else {
printf("服务器:暂时没有子进程退出\n");
break;
}
}
}
int client(int connfd) {
for (;;) {
printf("%d:接收请求\n", getpid());
char buf[1024];
ssize_t rb = recv(connfd, buf, sizeof(buf), 0);
if (rb == -1) {
perror("recv");
return -1;
}
if (rb == 0) {
printf("%d:客户机已关闭\n", getpid());
break;
}
printf("%d:发送响应\n", getpid());
if (send(connfd, buf, rb, 0) == -1) {
perror("send");
return -1;
}
}
return 0;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
if (signal(SIGCHLD, sigchld) == SIG_ERR) {
perror("signal");
return -1;
}
printf("服务器:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("服务器:准备地址并绑定\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
printf("服务器:侦听套接字\n");
if (listen(sockfd, 1024) == -1) {
perror("listen");
return -1;
}
for (;;) {
printf("服务器:等待连接\n");
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof(addrcli);
int connfd = accept(sockfd, (struct sockaddr*)&addrcli, &addrlen);
if (connfd == -1) {
perror("accept");
return -1;
}
printf("服务器:客户机%s:%u\n", inet_ntoa(addrcli.sin_addr),
ntohs(addrcli.sin_port));
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
if (close(sockfd) == -1) {
perror("close");
return -1;
}
if (client(connfd) == -1)
return -1;
if (close(connfd) == -1) {
perror("close");
return -1;
}
printf("%d:完成\n", getpid());
return 0;
}
if (close(connfd) == -1) {
perror("close");
return -1;
}
}
return 0;
}
/* tcpcli.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
if (argc < 3) {
fprintf(stderr, "用法:%s <IP地址> <端口号>\n", argv[0]);
return -1;
}
printf("客户机:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("客户机:准备地址并连接\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
return -1;
}
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]), stdin);
if (!strcmp(buf, "!\n"))
break;
printf("客户机:发送请求\n");
if (send(sockfd, buf, strlen(buf) * sizeof(buf[0]), 0) == -1) {
perror("send");
return -1;
}
printf("客户机:接收响应\n");
ssize_t rb = recv(sockfd, buf, sizeof(buf) - sizeof(buf[0]), 0);
if (rb == 0) {
printf("客户机:服务器已关闭\n");
break;
}
buf[rb / sizeof(buf[0])] = '\0';
printf("< %s", buf);
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("完成\n");
return 0;
}
4.基于迭代的UDP客户机/服务器模型
1)UDP协议的基本特征
A.面向无连接:参与通信的主机之间不需要专门建立和维护逻辑的连接通道。一个UDP套接字可以和任意其它UDP套接字通信,而不必受连接的限制。
B.不可靠传输:没有超时重传机制。可能导致数据丢失。
C.不保证顺序:没有序列号,也不进行序列重组。可能产生数据包重复和乱序。
D.无流量控制:没有通告窗口,通信参与者完全不知道对方的接受能力。可能造成数据溢出。
E.记录式传输:以消息报文的形式传输数据,数据包在传输过程中有记录边界。应用程序无需划分数据包边界。
F.全双工
通过在可靠性方面的部分牺牲换取高速度,在子网以及局域网内部可以不经过服务器点对点通信,还有广播机制。
例如:QQ,微信,在线电影
2)常用函数
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr,socklen_t addrlen);
dest_addr - 数据包接收者地址结构
addrlen - 数据包接收者地址结构字节数
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
src_addr - 输出数据包发送者地址结构
addrlen - 输出数据包发送者地址结构字节数
代码:udpsvr.c、udpcli.c
/* udpsvr.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
printf("服务器:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("服务器:准备地址并绑定\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
for (;;) {
printf("服务器:接收请求\n");
char buf[1024];
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof(addrcli);
ssize_t rb = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&addrcli, &addrlen);
if (rb == -1) {
perror("recvfrom");
return -1;
}
printf("服务器:向%s:%u发送响应\n", inet_ntoa(addrcli.sin_addr), ntohs(addrcli.sin_port));
if (sendto(sockfd, buf, rb, 0, (struct sockaddr*)&addrcli, addrlen) == -1) {
perror("sendto");
return -1;
}
}
return 0;
}
/* udpcli.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
if (argc < 3) {
fprintf(stderr, "用法:%s <IP地址> <端口号>\n", argv[0]);
return -1;
}
printf("客户机:创建网络套接字\n");
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("客户机:准备地址\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]), stdin);
if (!strcmp(buf, "!\n"))
break;
printf("客户机:向%s:%u发送请求\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
if (sendto(sockfd, buf, strlen(buf) * sizeof(buf[0]), 0, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("sendto");
return -1;
}
printf("客户机:接收响应\n");
ssize_t rb = recv(sockfd, buf, sizeof(buf) - sizeof(buf[0]), 0);
if (rb == -1) {
perror("recv");
return -1;
}
buf[rb / sizeof(buf[0])] = '\0';
printf("< %s", buf);
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("客户机:完成\n");
return 0;
}
针对UDP套接字的connect函数并不象TCP套接字的connect函数一样通过三路握手过程建立所谓的虚电路连接,而仅仅是将传递给该函数的对方地址结构缓存在套接字对象中。此后通过该套接字发送数据时,可以不使用sendto函数,而直接调用send函数,有关接收方的地址信息从套接字对象的地址缓存中提取即可。
5.域名解析(Domain Name Service, DNS)
字符串形式的域名
(www.tmooc.cn)
|
v
DNS服务器
|
v
整数形式的IP地址
(202.141.55.23)-->套接字编程
根据主机域名获取信息
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
成功返回主机信息条目指针,失败返回NULL。
name - 主机域名(字符串)
hostent
h_name -> xxx\0 - 主机官方名
h_aliases -> * * * ... NULL - 别名表
h_addrtype -> AF_INET - 地址类型
h_length -> 4 - 地址字节数
h_addr_list -> * * * ... NULL - 地址表
|
v
struct in_addr
s_addr - 网络字节序32位无符号整数
代码:dns.c
/* dns.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "用法:%s <主机域名>\n", argv[0]);
return -1;
}
struct hostent* host = gethostbyname(argv[1]);
if (!host) {
perror("gethostbyname");
return -1;
}
if (host->h_addrtype != AF_INET) {
fprintf(stderr, "非IPv4地址!\n");
return -1;
}
printf("主机官方名:\n");
printf("\t%s\n", host->h_name);
printf("主机别名表:\n");
for (char** pp = host->h_aliases; *pp; ++pp)
printf("\t%s\n", *pp);
printf("主机地址表:\n");
for (struct in_addr** pp = (struct in_addr**)host->h_addr_list; *pp; ++pp)
printf("\t%s\n", inet_ntoa(**pp));
return 0;
}
6.获取HTTP服务器上的页面内容
HTTP, Hyper Text Transform Protocol,超文本传输协议
www服务器通过http协议提供页面服务。
HTTP - 应用层协议:| HTTP包头 | HTTP包体 |
|
TCP - 传输层协议:| TCP包头 | HTTP包头 | HTTP包体 |
|
IP - 网络层协议:...
|
...
HTTP请求包头:
方法 资源路径 协议及
| (URI,统一资源定位符) 其版本
| | |
GET /user/project/main.html HTTP/1.1
键 值(主机域名)
| |
Host: www.tmooc.cn\r\n
Accept: text/html\r\n
Connection: Keep-Alive\r\n
User-Agent: Mozilla/5.0\r\n
Referer: www.tmooc.cn\r\n
...
最后一行\r\n\r\n
代码:http.c
/* http.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
// 解析命令行
if (argc < 3) {
fprintf(stderr, "用法:%s <主机IP地址> <主机域名> [<资源路径>]\n",
argv[0]);
return -1;
}
const char* ip = argv[1];
const char* domain = argv[2];
const char* path = argc < 4 ? "/" : argv[3];
// 创建套接字
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
// 服务器地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
if (!inet_aton(ip, &addr.sin_addr)) {
perror("inet_aton");
return -1;
}
// 连接服务器
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect");
return -1;
}
// 格式化请求
char request[1024];
sprintf(request,
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Accept: */*\r\n"
"Connection: Close\r\n"
"User-Agent: Mozilla/5.0\r\n"
"Referer: %s\r\n\r\n",
path, domain, domain);
printf("%s", request);
// 发送请求
if (send(sockfd, request, strlen(request) * sizeof(request[0]), 0) == -1) {
perror("send");
return -1;
}
// 接收响应
for (;;) {
char respond[1024] = {};
ssize_t rlen = recv(sockfd, respond, sizeof(respond) - sizeof(respond[0]), 0);
if (rlen == -1) {
perror("recv");
return -1;
}
if (!rlen)
break;
printf("%s", respond);
}
printf("\n");
close(sockfd);
return 0;
}