前言
学学协议设计,加深下理解(?
数据结构
sockaddr
结构:针对各种通信域的套接字,存储它们的地址信息
struct sockaddr {
u_short sa_family; // 地址家族
char sa_data[14]; // 协议地址
};
sockaddr_in
结构:专门针对 Internet 通信域,存储套接字相关的网络地址信息,例如 IP 地址,传输层端口号
官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-sockaddr_in
struct sockaddr_in {
short sin_family; // 地址家族
u_short sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 全为0
};
in_addr
结构:专门用来存储 IP 地址
typedef struct in_addr {
union {
struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { u_short s_w1, s_w2; } S_un_w;
u_long S_addr;
} S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;
用法:
首先定义一个 sockaddr_in 的结构实例,并将它清零;
然后再进行赋值;
函数调用时将这个结构强转为 sockaddr
struct sockaddr_in myad;
memset(&myad,0,sizeof(struct sockaddr_in));
myad.sin_family = AF_INET;
myad.sin_port = htons(8080);
myad.sin_addr.S_addr = htonl(INADDR-ANY);
accept(listenfd, (sockaddr*)(&myad), &addrlen);
本机字节顺序和网络字节顺序
本机字节顺序:在具体计算机中的多字节数据的存储顺序
网络字节顺序:多字节数据在网络协议报头中的存储顺序,套接字中必须用
所以装入套接字时要从本机字节顺序转为网络字节顺序,本机输出时要从网络字节顺序转换为本机字节顺序
- htons():短整数本机顺序转换为网络顺序,用于端口号
- htonl():长整数本机顺序转换为网络顺序,用于IP地址
- ntohs():短整数网络顺序转换为本机顺序,用于端口号
- ntohl():长整数网络顺序转换为本机顺序,用于IP地址
面向连接的套接字编程
服务端:
SOCKET():初始化一个 socket
WINSOCK_API_LINKAGE SOCKET WSAAPI socket(int af,int type,int protocol);
af:协议簇,如 PF_INET 表示 TCP/IP
#define AF_INET 2 #define PF_INET AF_INET
type:通信类型
#define SOCK_STREAM 1 // 面向连接 #define SOCK_DGRAM 2 // 无连接 #define SOCK_RAW 3 #define SOCK_RDM 4 #define SOCK_SEQPACKET 5
protocol:传输层协议,在 Internet 通信域中,一般取0,由系统决定
BIND():socket 绑定网络地址
WINSOCK_API_LINKAGE int WSAAPI bind(SOCKET s,const struct sockaddr *name,int namelen);
s:套接字描述符
*name:指针所指结构中保存着特定的网络地址,Internet 域中,网络地址 = IP 地址 + 传输层端口号
namelen:sockaddr 的结构长度,等于
sizeof(struct sockaddr)
LISTEN():监听
WINSOCK_API_LINKAGE int WSAAPI listen(SOCKET s,int backlog);
s:套接字描述符
backlog:等待连接队列的最大长度,最大为20,一般为5~10;如果缓冲区队列有空,就接收一个来自客户端的连接请求,否则拒绝
ACCEPT():接受客户端的连接请求
WINSOCK_API_LINKAGE SOCKET WSAAPI accept(SOCKET s,struct sockaddr *addr,int *addrlen);
s:套接字描述符
*addr:出口参数,调用完毕后指针放置客户端的网络地址
*addrlen:出口参数,调用时初始设置 addr 结构的长度,调用完毕返回客户端的网络地址长度
READ():读取客户端发送来的请求/命令数据
WRITE():向客户端发送响应数据
CLOSE():关闭 socket
客户端:
SOCKET()
CONNECT():向服务端发送连接请求
WINSOCK_API_LINKAGE int WSAAPI connect(SOCKET s,const struct sockaddr *name,int namelen);
类似于bind
WRITE()
READ()
CLOSE()
C/S实例
stdafx.h
:预编译头,是自定义的头文件,与项目的源代码文件存放在同一个文件夹,内容可以包含系统的头文件如 stdio.h、string.h 等,这样就不用重复引入了
#include <winsock2.h>
#include <Windows.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib,"xxx.lib")
:引入库文件,用于调用动态链接库;这里用于引入 winsock2.h 的库 ws2_32.lib,注意编译时要添加-lws2_32
选项
WSADATA:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-wsadata
初始化 winsock:https://learn.microsoft.com/zh-cn/windows/win32/winsock/initializing-winsock
服务端
定义完数据和套接字后就可以填入对应参数,依次进行 bind、listen、accept 等操作
#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')
int main(int argc, char const *argv[])
{
WSADATA wsaData;
SOCKET sockServer;
SOCKADDR_IN addrServer;
SOCKET sockClient;
SOCKADDR_IN addrClient;
WSAStartup(MAKEWORD(2, 2), &wsaData);
sockServer = socket(AF_INET, SOCK_STREAM, 0);
addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(6000);
bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
listen(sockServer, 5);
printf("服务器已启动:\n监听中...\n");
int len = sizeof(SOCKADDR);
char recvBuf[100];
sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
recv(sockClient, recvBuf, 100, 0);
printf("%s\n", recvBuf);
closesocket(sockClient);
WSACleanup();
return 0;
}
编译:
gcc server.c -o output/server.exe -lws2_32 -fexec-charset=GBK
客户端
注意,c语言中为了确保字符串的终止符\0
(空字符)也被发送,send 发送到服务端的长度需要 +1
#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')
int main(int argc, char const *argv[])
{
WSADATA wsaData;
SOCKET sockClient;
SOCKADDR_IN addrServer;
WSAStartup(MAKEWORD(2,2),&wsaData);
sockClient=socket(AF_INET,SOCK_STREAM,0);
addrServer.sin_family=AF_INET;
addrServer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrServer.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));
char message[30]="111 Hello Socket!";
send(sockClient,message,strlen(message)+1,0);
closesocket(sockClient);
WSACleanup();
return 0;
}
改进
在服务器显示连接上的客户机的 IP 地址和端口
服务端在 accept 的时候保留了客户端的信息 addrClient,只需要将其转格式输出即可
使用
inet_ntoa
把inet_addr
的网络字节序转换为 IP 地址,ntohs
对应客户端的htons
sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len); printf("客户端 %s:%d\n",inet_ntoa(addrClient.sin_addr),ntohs(addrClient.sin_port));
服务器也发送一条信息(Hello! Client Socket.)给客户机并显示
服务端:和客户端发给服务端消息一样的操作
char message[30] = "Hello! Client Socket."; send(sockClient, message, strlen(message) + 1, 0); printf("消息已发送给客户端\n");
客户端:和服务端接收消息一样的操作,从 sockClient 接收
char recvBuf[100]; recv(sockClient, recvBuf, 100, 0); printf("收到服务端消息:%s\n", recvBuf);
完成步骤 2 后,修改为:
- 客户机键盘输入某一数字(例如 5)并将其发送给服务器
- 服务器收到数字并显示,然后将该数字加 1(例如 5+1=6)并将结果回送给客户机
- 客户机收到数字并显示
先修改客户端发送逻辑:
int input; printf("请输入一个整数: "); scanf("%d", &input); send(sockClient, (char*)&input, sizeof(input), 0);
服务端接收逻辑:
int receivedNum; recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0); printf("收到客户端数字: %d\n", receivedNum);
接下来修改服务端发送逻辑:
int resultNum = receivedNum + 1; send(sockClient, (char*)&resultNum, sizeof(resultNum), 0);
客户端接收逻辑:
int receivedNum; recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0); printf("收到服务端数字: %d\n", receivedNum);
修改程序使得以下情况能正常运行:服务器程序不关闭,客户机程序可重复运行关闭,两端正常接收和发送消息
直接
while True
保持运行即可服务端需要持续运行的部分从 accept 开始,客户端则是从建立 socket 开始
服务端完整代码:
#include "stdafx.h" #pragma comment(lib, 'ws2_32.lib') int main(int argc, char const *argv[]) { WSADATA wsaData; SOCKET sockServer; SOCKADDR_IN addrServer; SOCKET sockClient; SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); WSAStartup(MAKEWORD(2, 2), &wsaData); sockServer = socket(AF_INET, SOCK_STREAM, 0); addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrServer.sin_family = AF_INET; addrServer.sin_port = htons(6000); bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR)); listen(sockServer, 5); printf("服务器已启动...\n监听中...\n"); while(1){ sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len); printf("客户端 %s:%d\n", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port)); // char recvBuf[100]; // recv(sockClient, recvBuf, 100, 0); // printf("收到客户端消息:%s\n", recvBuf); int receivedNum; recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0); printf("收到客户端数字: %d\n", receivedNum); // char message[30] = "Hello! Client Socket."; // send(sockClient, message, strlen(message) + 1, 0); int resultNum = receivedNum + 1; send(sockClient, (char*)&resultNum, sizeof(resultNum), 0); printf("已返回结果\n"); closesocket(sockClient); printf("客户端连接已关闭,等待下一个...\n"); } closesocket(sockClient); WSACleanup(); return 0; }
客户端完整代码:
#include "stdafx.h" #pragma comment(lib, 'ws2_32.lib') int main(int argc, char const *argv[]) { WSADATA wsaData; SOCKET sockClient; SOCKADDR_IN addrServer; int input; int receivedNum; WSAStartup(MAKEWORD(2, 2), &wsaData); while (1) { sockClient = socket(AF_INET, SOCK_STREAM, 0); addrServer.sin_family = AF_INET; addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); addrServer.sin_port = htons(6000); if (connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR)) == SOCKET_ERROR) { printf("连接失败"); continue; } // char message[30] = "111 Hello Socket!"; // send(sockClient, message, strlen(message) + 1, 0); printf("请输入一个整数 (输入-1退出): "); scanf("%d", &input); if (input == -1) break; send(sockClient, (char *)&input, sizeof(input), 0); // char recvBuf[100]; // recv(sockClient, recvBuf, 100, 0); // printf("收到服务端消息:%s\n", recvBuf); recv(sockClient, (char *)&receivedNum, sizeof(receivedNum), 0); printf("收到服务端数字: %d\n", receivedNum); } closesocket(sockClient); WSACleanup(); return 0; }
一台服务器,多台客户机的情况下,由于服务端只能同时处理一个端口上的连接,会出现其他客户端等待的情况
在 UNIX 系统下,可以使用
fork()
创建子进程解决这个问题
利用流式套接字实现文件传送
使用 VC++ 6.0
创建 Win32 Console Application 工程
服务端
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
//先检查文件是否存在
char *filename = "C:\\test.txt"; //文件名
FILE *fp = fopen(filename, "rb"); //以二进制方式打开文件
if(fp == NULL){
printf("Cannot open file, press any key to exit!\n");
system("pause");
exit(0);
}
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
listen(servSock, 20);
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
//循环发送数据,直到文件结尾
char buffer[BUF_SIZE] = {0}; //缓冲区
int nCount;
while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
send(clntSock, buffer, nCount, 0);
}
shutdown(clntSock, SD_SEND); //文件读取完毕,断开输出流,向客户端发送FIN包
recv(clntSock, buffer, BUF_SIZE, 0); //阻塞,等待客户端接收完毕
fclose(fp);
closesocket(clntSock);
closesocket(servSock);
WSACleanup();
system("pause");
return 0;
}
客户端
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int main(){
//先输入文件名,看文件是否能创建成功
char filename[100] = {0}; //文件名
printf("Input filename to save: ");
gets(filename);
FILE *fp = fopen(filename, "wb"); //以二进制方式打开(创建)文件
if(fp == NULL){
printf("Cannot open file, press any key to exit!\n");
system("pause");
exit(0);
}
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//循环接收数据,直到文件传输完毕
char buffer[BUF_SIZE] = {0}; //文件缓冲区
int nCount;
while( (nCount = recv(sock, buffer, BUF_SIZE, 0)) > 0 ){
fwrite(buffer, nCount, 1, fp);
}
puts("File transfer success!");
//文件接收完毕后直接关闭套接字,无需调用shutdown()
fclose(fp);
closesocket(sock);
WSACleanup();
system("pause");
return 0;
}
f7 编译,crtl+f5 运行
成功下载文件 test.txt 并保存为 save.txt
无连接的套接字编程
特点:
- 应用程序双方是不对等的,服务器要先行启动,处于被动的等待访问的状态,而客户机则可以随时主动地请求访问服务器
- 服务器进程将套接字绑定到指定的端口,客户端需要知道服务端的网络地址
- 客户端套接字使用动态分配的自由端口,不需要进行绑定
- 客户端必须先发送数据报,在数据报中携带双方的地址,服务端收到后才能知道客户机端的地址,才能给客户机端回送数据报
- 服务器可以接收多个客户端的数据
系统调用:
SENDTO():发送数据报,需要一个全相关的五元组信息(协议(UDP)、源IP地址、源端口号、目的IP地址和目的端口号)
WINSOCK_API_LINKAGE int WSAAPI sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);
s:发送方的数据报套接字描述符
buf:发送缓冲区的长度
flags:发送方式,一般为0
to:指针,指向的 sockaddr 结构包含接收方完整的地址
tolen:sockaddr 结构的长度
RECVFROM():接收数据报
WINSOCK_API_LINKAGE int WSAAPI recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);
C/S实例
在面向连接的基础上稍作修改即可
服务端
相比面向连接,这里多了一些异常处理,socket 的参数为 socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')
int main(int argc, char const *argv[])
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
SOCKET serSocket;
SOCKADDR_IN serAddr;
SOCKADDR_IN remoteAddr;
int nAddrLen = sizeof(SOCKADDR);
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serSocket == INVALID_SOCKET)
{
printf("socket error!");
return 0;
}
serAddr.sin_addr.S_un.S_addr = INADDR_ANY;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
if (bind(serSocket, (SOCKADDR *)&serAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
printf("bind error!");
closesocket(serSocket);
return 0;
}
while (1)
{
char recvData[255];
int ret = recvfrom(serSocket, recvData, 255, 0, (SOCKADDR *)&remoteAddr, &nAddrLen);
if (ret > 0)
{
recvData[ret] = 0x00;
printf("接受到一个连接:%s\r\n", inet_ntoa((remoteAddr.sin_addr)));
printf((recvData));
}
char *sendData = "一个来自服务端的UDP数据包\n";
sendto(serSocket, sendData, strlen(sendData), 0, (SOCKADDR *)&remoteAddr, nAddrLen);
}
closesocket(serSocket);
WSACleanup();
return 0;
}
客户端
#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')
int main(int argc, char const *argv[])
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
SOCKET sockClient;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
sockClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
SOCKADDR_IN sin;
sin.sin_family = AF_INET;
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sin.sin_port = htons(8888);
int len = sizeof(sin);
char *sendData = "来自客户端的数据包.\n";
sendto(sockClient, sendData, strlen(sendData), 0, (SOCKADDR *)&sin, len);
char recvData[255];
int ret = recvfrom(sockClient, recvData, 255, 0, (SOCKADDR *)&sin, &len);
if (ret > 0)
{
recvData[ret] = 0x00;
printf(recvData);
}
closesocket(sockClient);
WSACleanup();
return 0;
}
先启动服务端,再启动客户端