资源参考自网络
带有ID的文件
一个文件描述符只是一个和打开的文件相关联的整数,背后代表的意思可能如下:
socket()的返回值就是文件描述符
文件句柄,需要调用专门针对socket而设计的数据传输函数
特征
流格式套接字内部有一个缓冲区,通过socket传输的数据将保存在这个缓冲区中。接收端手法哦数据后并不是立即读取,只要数据不超过缓冲区容量,接收端可以在缓冲区被填满之后一次性读取,也可以分多次读取。(接收端可以控制数据的读取)
无连接的套接字,用SOCK_DGRAM表示
只管传输数据,不做数据校验。
优缺点:传输效率快但是不能保证数据的有效性(数据会丢失)
特点:
QQ视频聊天和语音聊天则使用SOCK_DRRAM来传输数据,首先先保证通信效率,降低延迟,视频和音频即使丢失一小部分数据,也不会对最终数据在终端的显示造成什么大的影响。
IP地址
MAC地址
端口号:
一台计算机可以同时提供多种网络服务(Web服务,FTP服务,SMTP服务),为区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号,不同计算机上的使用同一个端口通信的计算机可以通过这个端口号进行数据通信
#include
#include
#include
#include
#include
#include
#include
int main(){
// 1.配置好需要连接的目标主机的socket信息(IP地址和端口号)
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
// 2.创建套接字(用于打开socket通信流)
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 3.将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 4.进行socket监听,等待用户发起请求
listen(serv_sock, 20);
// 5.接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
/////////// 正常情况下:程序运行到accept()就会被阻塞,等待客户端发起请求
// 6.向客户端发送数据(通过socket流向远程写数据)
char str[] = "http://c.biancheng.net/socket/";
write(clnt_sock, str, sizeof(str));
//关闭套接字
close(clnt_sock);
close(serv_sock);
return 0;
}
#include
#include
#include
#include
#include
#include
int main(){
// 1.配置好客户端需要连接的主机的IP地址和端口号
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
// 2.创建套接字,创建socket连接对象
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 3.向上面配置好的地址(IP地址+端口号)发起请求
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 直到服务器传回数据后,connect() 才运行结束。
// 4.读取服务器传回的数据
char buffer[40]; // 问题来了,这个buffer的长度是不是足够呢??
// 很明显:当服务端的数据长度远超过这个Buffer的长度的时候客户端,则需要对服务端的数据进行相应的处理才能保证客户端能够读取到所有的服务端数据
read(sock, buffer, sizeof(buffer)-1);
// 打印查看服务端数据(当然要保证服务端传过来的数据是可见字符,不然没得看QAQ)
printf("Message form server: %s\n", buffer);
//关闭套接字
close(sock);
return 0;
}
关于AF_INET与PF_INET的相关介绍: AF_INET与PF_INET
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
int main(){
// 1.初始化 DLL
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
// 2.配置好需要连接的主机的IP地址和端口号
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
// 3. 创建socket套接字,用于通信
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
// 4. 将socket绑定到上面配置好的地址,用于和这个地址的应用程序进行通信
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
// 5.监听
listen(servSock, 20);
// 6.接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
// 7.向客户端发送数据(当然我可以不发送的)
char *str = "Hello World!";
send(clntSock, str, strlen(str)+sizeof(char), NULL); //这里是send,Linux中是write
//关闭套接字
closesocket(clntSock);
closesocket(servSock);
//终止 DLL 的使用
WSACleanup();
return 0;
}
#include
#include
#include
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
int main(){
// 1.初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 2.配置需要连接的远程主机的socket信息
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
// 3.创建套接字用于连接远程主机,使用TCP协议进行通信
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
// 4.连接远程主机
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
// 5.接收服务器传回的数据
// 单单一个buffer是不足以接收服务器发过来的所有数据的
char szBuffer[MAXBYTE] = {0};
recv(sock, szBuffer, MAXBYTE, NULL); //使用recv函数,Linux使用read函数
// 5.输出服务器发过来的数据
printf("Message form server: %s\n", szBuffer);
// 6.关闭套接字
closesocket(sock);
// 7.终止使用 DLL
WSACleanup();
system("pause");
return 0;
}
点评:上面每一次连接都要打开关闭连接,有点笨,可以专门做一个工具类来控制socket对象的打开和关闭,仅调用一次,收发数据抽象为功能函数,可以多次调用。可以将Socket资源的打开关闭以RAII的方式进行封装。
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6
in_port_t sin6_port; //(2)16位端口号
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具体的IPv6地址
uint32_t sin6_scope_id; //(4)接口范围ID
};
connect函数:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen); //Windows
服务端程序在listen
监听程序后面加上如下代码:
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
char buffer[BUF_SIZE] = {0}; //缓冲区
while(1){
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
int strLen = recv(clntSock, buffer, BUF_SIZE, 0); //接收客户端发来的数据
send(clntSock, buffer, strLen, 0); //将数据原样返回
closesocket(clntSock); //关闭套接字
memset(buffer, 0, BUF_SIZE); //重置缓冲区
}
write()/send()先将数据写入到缓冲区内部,再根据TCP协议将缓冲区中的数据发送到目标机器,一旦将数据写入缓冲区,函数就返回,不管后面的机器发送数据的过程。
read()/recv()函数从缓冲区读取数据,而不是从网络中读取数据。
缓冲区特性:
获取输入输出黄忠的大小:getsockopt()
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
使用write()/send()
发送数据时
write()/send()
会被阻塞,直到缓冲区中的数据被成功发送,缓冲区为空,才唤醒write()/send()
继续写入数据write()/send()
函数会被阻塞,直到数据发送完毕,才对缓冲区解锁,才将write()/send()
唤醒write()/send()
才能返回当使用read()/recv()
读取数据时:
read()/recv()
函数再次读取。**read()/recv()
函数才会返回,否则一直被阻塞对于Socket方式的数据发送和接收方式而言,数据的接收和数据发送是无关的,不管数据通过write()/send()
发送了多少次,都会尽可能多的发送数据,根据上面的Socket中的内容可以看出,在服务端,只要你缓冲区为空,我就唤醒write()/send()
进行数据的发送;而在服务端我则是等待read()/recv()
可用的情况才对缓冲区中的数据进行读取,如此便会出现服务端和客户端数据发送/接收数据速率不同步的问题。因此可能会出现数据的**粘包问题。**举例如下:例如客户端向服务器第一次发送 1,第二次发送 3,服务器可能当成 13 来处理。
可以在上面的章节"Windows系统Socket通信程序Demo"让服务器线程在接收客户端数据前等待足够长的一段时间,比如:Sleep(10000);
。可以很容易观测到客户端程序多次发送的数据在服务器端形成了"粘包问题"。
对于服务端程序,当读取到文件末尾时,fread()
函数会返回0,可以在while
中判断fread()
是否返回0来结束服务端程序接收客户端文件的功能。
#include
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
//先检查文件是否存在
char *filename = "D:\\send.avi"; //文件名
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;
}
对于客户端程序,通过判断recv()
函数是否返回0来结束while循环,表示文件接收完成。
recv()==0
的出现时机:当数据传输完成,客户端会受到服务器传来的FIN包,由此客户端就知道服务器不会再向自己传输数据,此时recv()
正好等于0。
关于FIN的介绍,可以查看谢希仁的计算机网络的TCP的握手关闭连接那一章节。
在服务端,可以显式调用shutdown()
函数来发送FIN包,该方式会等待服务器端输出缓冲区中的数据传输完毕,而服务器端直接调用close()/closesocket()
函数则会使得输出缓冲区中的数据失效,因此丢失了数据。
#include
#include
#include
#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;
}
大小端问题本质上是CPU中的数据存储方式问题
以4字节int
类型数据0x12345678
,存储该数据的内存首地址为0x20
来说
大端序:高位字节存放到地位地址,上述数据的存放则为
0x20
: 0x12
0x21
: 0x34
0x22
: 0x56
0x23
: 0x78
其中:表示“存放”的意思
小端序:高位字节存到放高位地址上,上述数据的存放为
0x20
: 0x78
0x21
: 0x56
0x22
: 0x34
0x23
: 0x12
默认顺序:主流Intel系列CPU为小端序,网络字节序为大端序。
所以对于收发数据的主机来说需要做如下处理:主机 A 先把数据转换成大端序再进行网络传输,主机 B 收到数据后先转换为自己的格式再解析。
通过该函数即可:**struct** hostent *gethostbyname(**const** char *hostname);
相关数据解构:
struct hostent{
char *h_name; //official hostname
char **h_aliases; //alias list
int h_addrtype; //host address type
int h_length; //address lenght
char **h_addr_list; //address list
}
关于h_aliases
和h_addr_list
的相关介绍:
h_aliases
: char**
alias #1
: char*
alisa #2
: char*
NULL
h_addr_list
:char**
IP addr #1
: char*
IP addr #2:
char*`IP addr #3:
char*`NULL
代码示例:
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main(){
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
struct hostent *host = gethostbyname("www.baidu.com");
if(!host){
puts("Get IP address error!");
system("pause");
exit(0);
}
//别名
for(int i=0; host->h_aliases[i]; i++){
printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
}
//地址类型
printf("Address type: %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
//IP地址
for(int i=0; host->h_addr_list[i]; i++){
printf("IP addr %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
}
system("pause");
return 0;
}
运行结果:
Aliases 1: www.baidu.com
Address type: AF_INET
IP addr 1: 61.135.169.121
IP addr 2: 61.135.169.125
UDP
协议的客户端和服务端实现UDP
:用户数据报协议,协议的具体介绍在谢希仁的计算机网络中有介绍,更具体的可以查询RFC文档来了解该协议的具体内容信息。使用UDP
协议进行通信的双方不需要建立连接。
发送数据:
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); //Linux
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen); //Windows
参数介绍:
sock
:用于传输 UDP
数据的套接字buf
:保存待传输数据的缓冲区地址nbytes
:带传输数据的长度(以字节计)flags
:可选项参数,若没有可传递 0to
:存有目标地址信息的sockaddr
结构体变量的地址addrlen
:传递给参数 to 的地址值结构体变量的长度接收数据:
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen); //Linux
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen); //Windows
参数介绍:
sock
:用于接收 UDP
数据的套接字buf
:保存接收数据的缓冲区地址nbytes
:可接收的最大字节数(不能超过 buf
缓冲区的大小)flags
:可选项参数,若没有可传递 0from
:存有发送端地址信息的 sockaddr
结构体变量的地址addrlen
:保存参数 from 的结构体变量长度的变量地址值Windows平台:
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
//绑定套接字
sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充
servAddr.sin_family = PF_INET; //使用IPv4地址
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
servAddr.sin_port = htons(1234); //端口
bind(sock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR));
//接收客户端请求
SOCKADDR clntAddr; //客户端地址信息
int nSize = sizeof(SOCKADDR);
char buffer[BUF_SIZE]; //缓冲区
while(1){
int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);
sendto(sock, buffer, strLen, 0, &clntAddr, nSize);
}
closesocket(sock);
WSACleanup();
return 0;
}
#include
#include
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
//服务器地址信息
sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充
servAddr.sin_family = PF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(1234);
//不断获取用户输入并发送给服务器,然后接受服务器数据
sockaddr fromAddr;
int addrLen = sizeof(fromAddr);
while(1){
char buffer[BUF_SIZE] = {0};
printf("Input a string: ");
gets(buffer);
sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&servAddr, sizeof(servAddr));
int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);
buffer[strLen] = 0;
printf("Message form server: %s\n", buffer);
}
closesocket(sock);
WSACleanup();
return 0;
}