网络套接字编程
注:本文为学习《C语言从入门到精通》时,对部分章节的总结
1、计算机网络基础
1.1、IP地址
IP地址由IP协议规定的32位的二进制数表示,最新的IPv6协议由128位表示。
32位的IP地址主要分为前缀和后缀两部分。前缀表示计算机所属的物理网络,后缀确定该网络上的唯一一台计算机。在互联网上,每一个物理网络都有唯一地网络号,根据网络号的不同,可以将IP地址分为5类,即A类、B类、C类、D类和E类。A、B和C类属于基本类,D类用于多播发送,E类属于保留。
类型 |
|
A类 | 0.0.0.0~127.255.255.255 |
B类 | 128.0.0.0~191.255.255.255 |
C类 | 192.0.0.0~223.255.255.255 |
D类 | 224.0.0.0~239.255.255.255 |
E类 | 240.0.0.0~247.255.255.255 |
其中有几个特殊IP地址:
- 网络地址:在IP地址中主机地址为0的表示网络地址,如128.111.0.0
- 广播地址:在网络号后跟所有位全是1的IP地址,表示广播地址
- 回送地址:127.0.0.1表示回送地址,用于测试
1.2、OSI七层参考模型
层次 | 名称 | 功能描述 |
第7层 | 应用层(Application) | 负责网络中应用程序与网络操作系统间的联系。 |
第6层 | 表示层(Presentation) | 用于确定数据交换的格式,负责设备之间所需要的字符集合数据的转换 |
第5层 | 会话层(Session) | 用户应用程序与网络层的接口,能够建立与其他设备的连接,即会话,并且能够对会话进行有效的管理 |
第4层 | 传输层(Transport) | 提供会话层和网络层之间的传输服务,该服务会从会话层获得数据,必要时对数据进行分割,然后将数据无误的传递到网络层 |
第3层 | 网络层(Network) | 能够将传输的数据封包,然后通过路由选择、分段组合的控制,将信息从源设备传送到目标设备 |
第2层 | 数据链路层(Data Link) | 修正传输过程中的错误信号 |
第1层 | 物理层(Physical) | 利用传输介质为数据链路层提供物理连接,规范了网络硬件的特性、规格和传输速度 |
1.3、地址解析
地址解析:将计算机的协议地址解析为物理地址,即MAC地址,又称为媒体访问控制地址。通常,在网络上由地址解析协议(ARP)来实现地址解析。
两台计算机通信过程:A主机IP:192.168.1.21 B主机IP:192.168.1.23
- 主机A从本地ARP缓存中查找IP为192.168.1.23对应的物理地址。可以用命令行“arp -a”查看本地ARP缓存
- 如果主机A在ARP缓存中没有发现192.168.1.23映射的物理地址,将发送ARP请求帧到本地网络上的所有主机,在ARP请求中包含了主机A的物理地址和IP地址
- 本地网络上的其他主机接收到ARP请求帧后,检查是否与自己的IP地址匹配,如果不匹配,则丢弃ARP请求。如果主机B发现与自己的IP地址匹配,则将主机A的物理地址和IP地址添加到自己的ARP缓存中,然后主机B将自己的物理地址和IP地址发送到主机A,当主机A接收到主机B发来的信息,将以这些信息更新ARP缓存
- 当主机B的物理地址确定后,主机A就可以与主机B进行通信了
1.4、域名系统
1.5、TCP/IP协议
TCP/IP协议:传输控制协议/网际协议
TCP/IP协议将网络分为4层
TCP/IP协议 | OSI参考模型 |
应用层(包括Telnet、FTP、SNTP协议) | 会话层、表示层和应用层 |
传输层(包括TCP、UDP协议) | 传输层 |
网络层(包括ICMP、IP、ARP等协议) | 网络层 |
数据链路层 | 物理层和数据链路层 |
TCP/IP协议是一个包含多种协议的协议簇,其中主要的有网际协议(IP)和传输控制协议(TCP)
1.TCP协议:是一种提供可靠数据传输的通用协议。是TCP/IP体系结构中传输层上的协议。在发送数据时,应用层的数据传输到传输层,加上TCP的首部,数据就构成了报文。报文是网际层IP的数据,再加上IP首部,就构成了IP数据报。TCP协议数据:
typedef struct HeadTCP {
WORD SourcePort; // 16位源端口号
WORD DePort; // 16位目的端口
DWORD SequenceNo; // 32位序号
DWORD ConfirmNo; // 32位确认序号
BYTE HeadLen; // 与Flag为一个组成部分,首部长度,占4位,
BYTE Flag; // 保留6位,6位标识,共16位
WORD WndSize; // 16位窗口大小
WORD CheckSum; // 16位校验和
WOED UrgPtr; // 16位紧急指针
} HEADTCP;
2.IP协议:又称网际协议,工作在网络层,主要提供无链接数据报传输。不保证数据报的发送,但可以最大限度地发送数据。
IP协议数据:
typedef struct HeadIP {
unsigned char headerlen:4; // 首部长度,占4位
unsigned char version:4; // 版本,占4位
unsigned char servertype; // 服务类型,占8位,即1个字节
unsigned short totallen; // 总长度,占16位
unsigned short id; // 与idoff构成标识,共占16位,
unsigned short idoff; // 前3位是标识,后13位是片偏移
unsigned char ttl; // 生存时间,占8位
unsigned char proto; // 协议,占8位
unsigned short checksum; // 首部检验和,占16位
unsigned int sourceIP; // 源IP地址,占32位
unsigned int destIP; // 目的IP地址,占32位
} HEADIP;
3.ICMP协议:又称网际控制报文协议。负责网络上设备状态的发送和报文检查,可以将某个设备的故障信息发送到其它设备上。
ICMP协议数据:
typedef struct HeadICMP {
BYTE Type; // 8位类型
BYTE Code; // 8位代码
WORD ChkSum; // 16位检验和
} HEADICMP;
4.UDP协议:面向无连接的协议,为应用程序提供一次性的数据传输服务,不提供差错恢复,不能提供数据重传,所以安全性略差
UDP协议数据:
typedef struct HeadUDP {
WORD SourcePort; // 16位源端口号
WORD DePort; // 16位目的端口
WORD Len; // 16位UDP长度
WORD ChkSum; // 16位UDP检验和
}HEADUDP;
1.6、端口
端口:一个16位的无符号整数值来表示的,通信的应用程序(进程)。围0~65535,低于256的端口被作为系统的保留端口,用于系统进程的通信,不在这范围的端口号被称为自由端口。
1.7、套接字的引入
套接字存在于通信区域(也称为地址族)中,主要用于间通过套接字通信的进程的公有特性综合在一起。套接字通常只与同一区域的套接字交换数据。
1.8、网络字节顺序
TCP/IP协议使用16位整数和32位整数的高位先存格式。
2、套接字基础
套接字是网络通信的基石,是网络通信的基本构件。
2.1、套接字概述
套接字实际上是一个指向传输提供者的句柄。可分为原始套接字、流式套接字和数据包套接字。
- 原始套接字:是在WinSock2规范中提出的,能够使程序开发人员对底层的网络传输机制进行控制,在原始套接字下接收的数据中包含IP头
- 流式套接字:提供双向、有序、可靠的数据传输服务。该类型套接字在通信前需要双方建立连接,TCP协议采用的就是流式套接字
- 数据包套接字:提供双向的数据流,但不能保证数据传输的可靠性、有序性、和无重复型。UDP协议采用的就是数据包套接字
2.2、TCP的套接字的socket编程
TCP是面向连接的可靠的传输协议。利用TCP协议进行通信时,首先要建立通信双方的连接。一旦连接建立完成,就可以进行通信。TCP提供了数据确认和数据重传的机制,保证了发送的数据一定能到达通信的对方。
基于TCP面向连接的socket编程的服务器端程序流程:
- 创建套接字socket
- 将创建的套接字绑定(bind)到本地的地址和端口上
- 设置套接字的状态位监听状态(listen),准备接受客户端的连接请求
- 接受请求(accpet),同时返回得到一个用于连接的新套接字
- 使用这个新套接字进行通信(通信函数使用send/recv)
- 通信完毕,释放套接字资源(closesocket)
基于TCP面向连接的socket编程的客户端程序流程:
- 创建套接字socket
- 向服务器发出连接请求(connect)
- 请求链接后与服务器进行通信操作(send/recv)
- 释放套接字资源(closesocket)
2.3、UDP的套接字的socket编程
UDP是无连接的不可靠的传输协议。采用UDP进行通行时,不需要建立连接,可直接向一个IP地址发送数据,但不能保证对方能收到。
基于UDP面向无连接的套接字编程来说,服务端和客户端概念不是很严格。可以把服务器称为接收端,客户端就是发送数据的发送端。
基于UDP面向无连接的socket编程的发送端程序流程:
- 创建套接字socket
- 将套接字绑定(bind)到一个本地地址和端口上
- 等待接收数据(recvfrom)
- 释放套接字资源(closesocket)
基于UDP面向无连接的socket编程的接收==接收端程序流程:
- 创建套接字socket
- 向服务器发送数据(sendto)
- 释放套接字资源(closesocket)
3、套接字函数
3.1、套接字函数介绍
1.WSAStartup函数:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested:表示调用的Windows Socket版本,高字节记录修订版本,低字节记录主版本。版本为2.1,高字节记录1,低字节记录2
lpWSAData:是一个WSADATA结构指针
typedef struct WSAData {
WORD wVersion; //调用者使用的WS2_32.DLL动态库的版本号
WORD wHighVersion; //WS2_32.DLL支持的最高版本,通常与wVersion相同
char szDescription[WSADESCRIPTION_LEN + 1]; //套接字的描述信息
char szSystemStatus[WSASYS_STATUS_LEN + 1]; //系统的配置或状态信息
unsigned short iMaxSockets; //最多可打开的套接字个数
unsigned short iMaxUdpDg; //数据报的最大长度
char FAR* lpVendorInfo; //套接字厂商信息
} WSADATA, FAR * LPWSADATA;
作用:初始化Ws2_32.dll动态链接库。在使用套接函数之前,一定要初始化Ws2_32.dll动态链接库。
2.socket函数
SOCKET socket(int af, int type, int protocol);
af:一个地址家族,通常为AF_INET
type:套接字类型。SOC_STREAM表示创建面向连接的流式套接字;SOCK_DGRAM表示创建面向无连接的数据报套接字;SOCK_RAW表示创建原始套接字
protocol:套接字所用的协议,如果不指定,可设置为0
返回值:创建的套接字句柄
功能:创建一个套接字
3.bind函数
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);3
s:套接字标识
name:一个sockaddr结构指针,结构中包含了要结合的地址和端口号
namelen:确定name缓冲区的长度
返回值:函数执行成功,返回0;否则为SOCKET_ERROR
功能:将套接字绑定到指定的端口和地址上
4.listen函数
int listen(SOCKET s, int backlog);
s:套接字标识
backlog:表示等待连接的最大队列长度。比如值为2,此时有3个客户端同事发出连接请求,那么前两个连接会放置在等待队列中,第3个客户端会得到错误信息
功能:将套接字设置为监听模式。对于流式套接字,必须处于监听模式才能够接收客户端套接字的连接
5.accept函数
SOCKET accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
s:套接字标识,应处于监听状态
addr:sockaddr_in结构指针,包含一组客户端的端口号、IP地址等信息
addrlen:参数addr的长度
返回值:一个新的套接字,对应于已经接收的客户端连接,对于客户端的所有后续操作,都应使用这个套接字
功能:接收客户端的连接。流式套接字必须处于监听状态才能接收客户端的连接
6.closesocket函数
int closesocket(SOCKET s);
s:套接字标识。如果参数s设置了SO_DONTLINGER选项,则调用该函数后会立即返回。此时如果有数据尚未传送完毕,则会继续传递数据,然后再关闭套接字
功能:关闭套接字
7.connect函数
int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);
s:套接字标识
name:套接字s要连接的主机地址和端口号
namelen:name参数的缓冲区长度
返回值:函数执行成功,返回0;否则返回SOCKET_ERROR。用户可以通过WSAGETLASTERROR得到其错误描述
功能:发送一个连接请求
8.htons函数
u_short htons(u_short hostshort);
hostshort:一个主机排列方式的无符号短整型数据
返回值:16位的网络排列方式数据
功能:将一个16位无符号短整形数据由主机排列方式转换为网络排列方式
9.htonl函数
u_long htonl(u_long hostlong);
hostlong:一个主机排列方式的无符号长整型数据
返回值:32位的网络排列方式数据
功能:将一个32位无符号长整形数据由主机排列方式转换为网络排列方式
10.inet_addr函数
unsigned long inet_addr(const char FAR* cp);
cp:一个IP地址的字符串
返回值:32位无符号长整数
功能:将一个由字符串表示的地址转换为32位的无符号长整形数据
11.recv函数
int recv(SOCKET s, char FAR* buf, int len, int flags);
s:套接字标识
buf:接收数据的缓冲区
len:buf的长度
flags:函数的调用方式。MSG_PEEK:查看传来的数据,在序列前端的数据会被复制一份到返回缓冲区中,但是这个数据不会从序列中移走;MSG_OOB:用来处理Out-Of-Band数据,也就是外带数据
功能:从面向连接的套接字中接收数据
12.send函数
int send(SOCKET s, char FAR* buf, int len, int flags);
s:套接字标识
buf:存放要发送数据的缓冲区
len:buf的长度
flags:函数的调用方式
功能:在面向连接的套接字间发送数据
13.recvfrom函数
int recvfrom(SOCKET s, char FAR* buf, int len, int flags, struct sockeraddr FAR* from, int FAR* fromlen);
s:准备接收数据的套接字
buf:接收数据的缓冲区
len:buf长度
flags:通过设置该值可以影响函数的调用行为
from:指向地址结构的指针,用来接收发送数据方的地址信息
fromlen:from的长度
功能:用于接收一个数据报信息并保存源地址
14.sendto函数
int sendto(SOCKET s, char FAR* buf, int len, int flags, const struct sockeraddr FAR* to, int FAR* tolen);
s:准备接收数据的套接字
buf:要发送数据的缓冲区
len:buf长度
flags:通过设置该值可以影响函数的调用行为
to:指定目标套接字的地址
tplen:to的长度
功能:用于接收一个数据报信息并保存源地址
15.WSACleanup函数
int WSACleanup(void);
功能:释放为Ws2_32.dll动态链接库初始化时分配的资源
3.2、基于TCP的网络聊天程序
// 网络聊天服务端的程序
#include <stdio.h>
#include <windock.h>
int main()
{
//定义变量
char sendBuf[100]; //发送数据的buf
char receiveBuf[100]; //接受数据的buf
int sendLen; //发送数据的长度
int receiveLen; //接收数据的长度
int length; //表示SOCKADDR的大小
SOCKET socketServer; //定义服务器套接字
SOCKET socketReceive; //定义用于连接套接字
SOCKADDR_IN serverAdd; //服务器地址信息结构
SOCKADDR_IN clientAdd; //客户端地址信息结构
WORD wVersionRequested; //字(word):unsigned short
WSADATA wsaData; //库版本信息结构
int error; //表示错误
//初始化套接字库
//定义版本类型。将两个字节组合成一个字,前面是低字节,后面是高字节
wVersionRequested = MAKEWORD(2, 2);
//加载套接字库,初始化Ws2_32.dll动态链接库
error = WSAStartUp(wVersionRequested, &wsaData);
if (error != 0) {
printf("加载套接字失败!\n");
return 0;
}
//判断请求加载的版本号是否符合要求
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
WSACleanup(); //不符合,关闭套接字库
return 0;
}
//设置连接地址
serverAdd.sin_family = AF_INET; //地址家族,必须是AF_INET,注意只有它不是网络字节顺序
serverAdd.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //主机地址
serverAdd.sin_port = htons(5000); //端口号
//创建套接字
//AF_INET表示指定地址族,SOCK_STREAM表示流式套接字TCP,特定的地址家族相关的协议
socketServer = socket(AF_INET, SOCK_STREAM, 0);
//绑定套接字到本地的某个地址和端口上
if (bind(socketServer, (SOCKADDR*)&serverAdd, sizeof(SOCKADDR)) == SOCKET_ERROR) {
printf("绑定失败!\n");
}
//设置套接字位监听状态
if (listen(socketServer, 5) < 0) {
printf("监听失败!\n");
}
//接受连接
length = sizeof(SOCKADDR);
//接收客户端的发送请求,等待客户端发送connect请求
socketReceive = accept(socketServer, (SOCKADDR*)&clientAdd, &length);
if (socketReceive == SOCKET_ERROR) {
printf("接受连接失败!\n");
}
//进行聊天
while (1) {
//接收数据
receiveLen = recv(socketReceive, receiveBuf, 100, 0);
if (receiveLen < 0) {
printf("接收失败,程序退出!\n");
break;
} else {
printf("客户端说:%s\n", receiveBuf);
}
//发送数据
printf("请发送信息:\n");
scanf("%s", sendBuf);
sendLen = send(socketReceive, sendBuf, 100, 0);
if (sendLen < 0) {
printf("发送失败!\n");
}
}
//释放套接字,关闭动态库
closesocket(socketReceive); //释放客户端的套接字资源
closesocket(socketServer); //释放套接字资源
WSACleanup(); //关闭动态链接库
return 0;
}
// 网络聊天客户端的程序
#include <stdio.h>
#include <windock.h>
int main()
{
//定义变量
char sendBuf[100]; //发送数据的buf
char receiveBuf[100]; //接受数据的buf
int sendLen; //发送数据的长度
int receiveLen; //接收数据的长度
SOCKET socketSend; //定义套接字
SOCKADDR_IN serverAdd; //服务器地址信息结构
WORD wVersionRequested; //字(word):unsigned short
WSADATA wsaData; //库版本信息结构
int error; //表示错误
//初始化套接字库
//定义版本类型。将两个字节组合成一个字,前面是低字节,后面是高字节
wVersionRequested = MAKEWORD(2, 2);
//加载套接字库,初始化Ws2_32.dll动态链接库
error = WSAStartUp(wVersionRequested, &wsaData);
if (error != 0) {
printf("加载套接字失败!\n");
return 0;
}
//判断请求加载的版本号是否符合要求
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
WSACleanup(); //不符合,关闭套接字库
return 0;
}
//设置服务器地址
serverAdd.sin_family = AF_INET; //地址家族,必须是AF_INET,注意只有它不是网络字节顺序
serverAdd.sin_addr.S_un.S_addr = inet_addr("192.168.1.43"); //主机地址
serverAdd.sin_port = htons(5000); //端口号
//进行连接服务器
//AF_INET表示指定地址族,SOCK_STREAM表示流式套接字TCP,特定的地址家族相关的协议
socketSend = socket(AF_INET, SOCK_STREAM, 0);
//客户端发送connect请求
if (connect(socketSend, (SOCKADDR*)&ServerAdd, sizeof(SOCKADDR)) == SOCKET_ERROR) {
printf("连接失败!\n");
}
//进行聊天
while (1) {
//发送数据
printf("请发送信息:\n");
scanf("%s", sendBuf);
sendLen = send(socketSend, sendBuf, 100, 0);
if (sendLen < 0) {
printf("发送失败!\n");
}
//接收数据
receiveLen = recv(socketSend, receiveBuf, 100, 0);
if (receiveLen < 0) {
printf("接收失败,程序退出!\n");
break;
} else {
printf("服务器端说:%s\n", receiveBuf);
}
}
//释放套接字,关闭动态库
closesocket(socketSend); //释放套接字资源
WSACleanup(); //关闭动态链接库
return 0;
}