1. 网络和网络协议
1) 什么是计算机网络?
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过有形或无形的通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
2) 什么是网络协议?
网络协议是一种特殊的软件,是计算机网络实现其功能的最基本的机制。网络协议的本质就是规则,即各种硬件和软件必须遵循的共同守则。网络协议并不是一套单独的软件,它融合于所有涉及网络通信的软件甚至硬件之中,因此可以说协议于网络中无处不在。
3) 什么是协议栈?
为了减少网络设计的复杂性,绝大多数网络采用分层设计的方法。所谓分层设计,就是按照信息流动的过程将网络的整体功能分解为一个个的功能层,不同机器上的同等功能层之间采用相同的协议,同一机器上相邻功能层之间通过接口进行信息传递。各层的协议和接口统称为协议栈。
ISO(国际标准化组织)提出OSI(Open System Interconnection, 开放系统互联)网络协议模型:
应用层:业务逻辑
表示层:数据的表现形式(二进制流、文本、MP3、MP4等等)
会话层:建立、管理和终止通信过程
传输层:源到目的地的点对点传输
网络层:路径选择、路由、寻址等网络结构拓扑
数据链路层:物理寻址、数据通道、错误检测等通信路径
物理层:在数据和电平信号之间进行转换
(应用层、表示层、会话层广义上也叫应用层)
比喻:
买点心(应用层:业务需求)
选择包装(表示层:数据形式)
选择快递公司(会话层:通信过程)
填写收寄单(传输层:点对点)
选择路径(网络层:通路)
中途周转(数据链路层:多点连线)
送货过程(物理层:实际通信)
4) TCP/IP协议栈
应用层:HTTP、FTP、SMTP
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:ARP、RARP
具体可看这篇文章:link.
5) 消息包和数据流
应用层:HTTP请求=用户数据包
传输层:TCP头+用户数据包=TCP包
网络层:IP头+TCP包=IP包
链路层:以太网头+IP包+以太网尾=以太网帧
物理层:以太网帧->电平信号
| ^
v |
传输线路
发送数据流:消息自协议栈顶层向底层流动,逐层打包。
接收数据流:消息自协议栈底层向顶层流程,逐层解析。
6) IP地址
IP地址(Internet Protocol Address,互联网协议地址)是一种地址格式,为互联网上的每个网络和主机分配一个逻辑地址,其目的是消除物理地址的差异性。
IP地址的计算机内部用一个网络字节序的32位(4个字节)无符号整数表示。通常习惯将其表示为点分十进制整数字符串的形式。
例如:
点分十进制整数字符串:192.168.1.1
32位(4个字节)无符号整数:0xC0A80101网络字节序就是大端字节序,高位在低地址,低位在高地址。
内存布局:| 0xC0 | 0xA8| 0x01 | 0x01 |
低地址--------------->高地址
一台计算机的IP地址=网络地址+主机地址
A级地址:以0为首8位网络地址+24位主机地址
B级地址:以10为首16位网络地址+16位主机地址
C级地址:以110为首24位网络地址+8位主机地址
D级地址:以1110为首的32为多(组)播地址例如:某台计算机的IP地址为192.168.182.48,其网络地址和主机地址分别为何?
192 168 182 48
11000000 10101000 10110110 00110000
以110为首,C级地址,网络地址是192.168.182.0,主机地址是48。
主机IP地址 & 子网掩码 = 网络地址
主机IP地址 & (~子网掩码) = 主机地址例如:主机IP地址192.168.182.48,子网掩码255.255.255.0,其网络地址和主机地址分别为何?
192.168.182.48 & 255.255.255.0 = 192.168.182.0
192.168.182.48 & 0.0.0.255 = 0.0.0.48
2. 套接字
1) 什么是套接字?
一个由系统内核负责维护,通过文件描述符访问的对象, 可用于在同一台机器或不同机器中的进程之间实现通信。
进程表项
文件描述符表
0: 文件描述符标志 | * -> 标准输入文件表项 -> 键盘
1: 文件描述符标志 | * -> 标准输出文件表项 -> 显示器
2: 文件描述符标志 | * -> 标准错误文件表项 -> 显示器
3: 文件描述符标志 | * -> 套接字对象 -> 网卡
应用程序 应用程序
v v
磁盘文件的文件描述符 表示网络的文件描述符
v v
文件对象 套接字对象
v v
文件系统 网络协议栈
v v
磁盘设备 网络设备
套接字也可以被视为是围绕表示网络的文件描述符的一套函数库。调用其中的函数就可以访问网络上的数据,实现不同主机间的通信功能。
2) 绑定和连接
套接字就是系统内核内存中的一块数据——逻辑对象
| 绑定(bind)
包含了IP地址和端口号等参数的网络设备——物理对象
通过IP地址(网络地址+主机地址)和端口号就可以唯一定位互联网上的一个通信引用。
互联网
| <-网络地址:192.168.182.0(找到子网络)
子网络
| <-主机地址:0.0.0.48(找到子网络中的哪一台设备)
计算机
| <-端口号:80(找到子网络中的哪一台设备的哪一个应用/进程)
应用
主机A
应用程序
|
逻辑对象(套接字)
| 绑定(bind) 连接(connection)
物理对象(IP地址和端口号)---------物理对象(IP地址和端口号)
| 绑定(bind)
逻辑对象(套接字)
|
应用程序
主机B
3) 常用函数
socket函数
创建套接字
#include <sys/socket.h> int socket(int domain, int type, int protocol);
成功返回套接字描述符,失败返回-1。
domain - 通信域,即协议族,可取以下值:
PF_LOCAL 或 PF_UNIX:本地通信,进程间通信
PF_INET:互联网通信
PF_PACKET:底层包通信(嗅探器、端口扫描)
type - 套接字类型,可取以下值:
SOCK_STREAM:流式套接字,使用TCP协议
SOCK_DGRAM:数据报式套接字,使用UDP协议
SOCK_RAW:原始套接字,使用自定义协议
protocol - 特殊协议,对于流式套接字和数据报式套接字,取0
套接字描述符与文件描述符在逻辑层面是一致的,所有关于文件描述符的规则对于套接字描述符也同样成立。同样也通过close函数关闭套接字,即释放内核中的有关资源。
基本地址结构:
struct sockaddr { sa_family_t sa_family; // 地址族 char sa_data[14]; // 地址值 }; 基本地址结构仅用于给函数传参时做强制类型转换。 根据sa_family接收到不同的值,然后调用不同的地址结构(本地 / 网络)的字段属性。 这个过程类型类似重载。
本地地址结构:
#include <sys/un.h> struct sockaddr_un { sa_family_t sun_family; // 地址族 // (AF_LOCAL/AF_UNIX) char sun_path[]; // 套接字文件路径 };
网络地址结构:
#include <sys/in.h> struct sockaddr_in { sa_family_t sin_family; // 地址族(AF_INET) in_port_t sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IP地址 }; struct in_addr { in_addr_t s_addr; // 网络字节序32位无符号整数形式的IP地址 };
typedef uint32_t in_addr_t; typedef uint16_t in_port_t;
为了解决两主机之间大小端字节序不一致的问题:
统一在发送的时候将发送主机的字节序转换为网络(大端)字节序,在接收时再将网络(大端)字节序转换为接收主机的字节序。发送:主机字节序->网络(大端)字节序
接收:网络(大端)字节序->主机字节序
bind函数
将套接字对象和自己的地址结构绑定在一起
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 自己的地址结构:
addr->sa_family: AF_LOCAL / AF_UNIX
作类型转换((struct sockaddr_un*)addr)->sun_path: 套接字文件
addr->sa_family: AF_INET
作类型转换((struct sockaddr_in*)addr)->sin_port/sin_addr: IP地址和端口号
addrlen - 地址结构字节数
connect函数
将套接字对象所代表的物理对象和对方的地址结构连接在一起
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 对方的地址结构
addrlen - 地址结构字节数
read函数 write函数
通过套接字描述符接收和发送数据的过程完全与通过文件描述符读取和写入数据的过程完全一样。
ssize_t read(int sockfd, void* buf, size_t count); ssize_t write(int sockfd, const void* buf, size_t count);
字节序转换(数据的转换)
通过网络传输多字节整数,需要在发送前转换为网络字节序,在接收后转换为主机字节序。
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
h - host,主机(字节序)
to - 到,把…转换到…
n - network,网络(字节序)
l - long版本,32位无符号整数
s - short版本,16位无符号整数
IP地址转换
(网络字节序32位无符号)整数<-------->(点分十进制)字符串
#include <arpa/inet.h> in_addr_t inet_addr(const char* cp); // 串->数 返回转换结果 int inet_aton(const char* cp, struct in_addr* inp);// 串->数 转换成功返回0,失败返回-1。将转换结果放到inp中
char* inet_ntoa(struct in_addr in); // 数->串 转换成功返回字符串指针,失败返回NULL。
服务器:提供业务服务的计算机程序。
客户机:请求业务服务的计算机程序。基于本地套接字的进程间通信:
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_un) 准备地址结构>(sockaddr_un)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(…) 等待处理(…)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:locsvr.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "mysock"
int main(void)
{
printf("服务器:创建本地套接字\n");
int sockfd = socket(PF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("服务器:准备地址并绑定\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, SOCK_FILE);
if (bind(sockfd, (struct sockaddr*)&addr,
sizeof(addr)) == -1) {
perror("bind");
return -1;
}
printf("服务器:接收数据\n");
for (;;) {
char buf[1024] = {};
ssize_t rb = read(sockfd, buf, sizeof(//read在UDP中无法得知发送方是谁,所以是单向的,除非使用下面的recvfrom
buf) - sizeof(buf[0]));
if (rb == -1) {
perror("read");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
printf("< %s", buf);
}
printf("服务器:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("服务器:删除套接字文件\n");
if (unlink(SOCK_FILE) == -1) {
perror("unlink");
return -1;
}
printf("服务器:完成\n");
return 0;
}
代码:loccli.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "mysock"
int main(void)
{
printf("客户机:创建本地套接字\n");
int sockfd = socket(PF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
printf("客户机:准备地址并连接\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, SOCK_FILE);
if (connect(sockfd, (struct sockaddr*)&addr,
sizeof(addr)) == -1) {
perror("connect");
return -1;
}
printf("客户机:发送数据\n");
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]),
stdin);
if (write(sockfd, buf, strlen(buf) *
sizeof(buf[0])) == -1) {
perror("write");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("客户机:完成\n");
return 0;
}
基于网络套接字的进程间通信:
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_in) 准备地址结构(sockaddr_in)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(…) 等待处理(…)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:netsvr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.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;//INADDR_ANY任何ip
if (bind(sockfd, (struct sockaddr*)&addr,
sizeof(addr)) == -1) {
perror("bind");
return -1;
}
printf("服务器:接收数据\n");
for (;;) {
char buf[1024] = {};
ssize_t rb = read(sockfd, buf, sizeof(
buf) - sizeof(buf[0]));
if (rb == -1) {
perror("read");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
printf("< %s", buf);
}
printf("服务器:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("服务器:完成\n");
return 0;
}
代码:netcli.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]);
if (connect(sockfd, (struct sockaddr*)&addr,
sizeof(addr)) == -1) {
perror("connect");
return -1;
}
printf("客户机:发送数据\n");
for (;;) {
printf("> ");
char buf[1024];
fgets(buf, sizeof(buf) / sizeof(buf[0]),
stdin);
if (write(sockfd, buf, strlen(buf) *
sizeof(buf[0])) == -1) {
perror("write");
return -1;
}
if (!strcmp(buf, "!\n"))
break;
}
printf("客户机:关闭套接字\n");
if (close(sockfd) == -1) {
perror("close");
return -1;
}
printf("客户机:完成\n");
return 0;
}
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包头 <20字节>| TCP包体 |
TCP包头含有6个标志位:SYN/ACK/FIN/RST/…和发送序列号和应答序列号
数据传输:超时重传、流量控制、面向字节流、全双工
终止连接:
客户机 服务器
主动关闭 ---------FIN(n)-------> 被动关闭
等待应答 <-----ACK(n+1)------ 关闭应答
确定关闭 <--------FIN(m)------- 已经关闭
关闭应答 ------ACK(m+1)-----> 等待应答
3)常用函数
listen函数
在指定套接字上启动对连接请求的侦听,即将该套接字置为被动模式,因为套接字都缺省为主动模式。
int listen(int sockfd, int backlog);
成功返回0,失败返回-1。
sockfd - 套接字描述符
backlog - 未决(未被答复的)连接请求队列的最大长度
accept函数
在指定的侦听套接字上等待并接受连接请求
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)- -+
recv函数
接收数据
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
flags - 接收标志,取0等价于read
MSG_DONTWAIT:非阻塞接收。对于阻塞模式,当接收缓冲区为空时,该函数会阻塞,直到接收缓冲区 不空为止。如果使用了此标志位,当接收缓冲区为空时,该函数会返回-1,并置errno 为EAGAIN或EWOULDBLOCK。
MSG_OOB:接收带外数据(不排队的数据,能使其先被发送,send方也要有此标志位)。
MSG_PEEK:瞄一眼数据,只将接收缓冲区中的数据复制到buf缓冲区中,但并不将其从接收缓冲区中删 除。
MSG_WAITALL:接收到所有期望接收的数据才返回,如果接收缓冲区中的数据不到len个字节,该函数会 阻塞,直到可接收到len个字节为止。当已经设置了非阻塞模式,这个阻塞效果失效并返 回 -1,
send函数
发送数据
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
flags - 接收标志,取0等价于write
MSG_DONTWAIT: 非阻塞发送。对于阻塞模式,当发送缓冲区的空余空间不足以容纳期望发送的字节数 时,该函数会阻塞,直到发送缓冲区的空余空间足以容纳期望发送的字节数为止。如 果使用了此标志位,能发送多少字节就发送多少字节,不会阻塞,甚至可能返回0表 示发送缓冲区满,无法发送数据。
MSG_OOB: 发送带外数据(不排队的数据,能使其先被发送,recv方也要有此标志位)。
MSG_DONTROUT: 不查路由表,直接在本地网中寻找目的主机。(用于无外网的情况,提高效率)
代码: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.全双工
通过在可靠性方面的部分牺牲换取高速度。
2) 常用函数
sendto函数
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
dest_addr - 数据包接收者地址结构
addrlen - 数据包接收者地址结构字节数
recvfrom函数
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
src_addr - 输出数据包发送者地址结构
addrlen - 输出数据包发送者地址结构字节数
代码: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函数,有关接收方的地址信息从套接字对象的地址缓存中提取即可。
代码:concli.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]);
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("客户机:向%s:%u发送请求\n",
inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port));
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 == -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;
}
5. 域名解析(Domain Name Service, DNS)
字符串形式的域名(www.baidu.com)通过DNS服务器,转化为整数形式(202.108.22.5)的IP地址,进而进行套接字编程。
gethostbyname函数
根据主机域名获取信息
#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
#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.baidu.cn\r\n
Accept: text/html\r\n
Connection: Keep-Alive\r\n
User-Agent: Mozilla/5.0\r\n
Referer: www.baidu.cn\r\n
…
PS:最后一行一定是\r\n\r\n,且每一行结尾都是\r\n
代码: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"//服务器自己关,因此接收到的字符数为0时表示结束
"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;
}