前言

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
-百度说的


正文

很好,百度说的太抽象看不懂。


c/s模式

就是客户端和服务端

  • 服务端
    • 首先服务器启动之后,根据请求提供相应的服务。
    • 打开一个通信通道,在某一地址和端口上接受请求。
    • 等待客户请求达到该端口
    • 接收到重复服务请求,处理该请求并发送应答信号。
    • 返回第二部,等待另一客户请求
    • 关闭服务器。
  • 客户端
    • 打开一个通信通道,并连接到服务器所在主机的特定端口。
    • 向服务器发送服务请求,等待并接受应答;继续提出请求。
    • 请求结束后关闭通信通道并终止。

常见端口,如http服务端口号为80,https为443等


ip地址和端口号

win+r打开运行,输入cmd回车都是些基本操作了。

windows用ipconfig
linux用ifconfig
就能查看最基本的几个网卡的信息。

ip通常指的是网络协议,ip地址则是具体的表现。分为ipv4和ipv6。
端口则是为了区分创建的套接字而分配的序号,把IP地址看成房子,端口则是出入的门。
端口号可以有65536[即2^16],其中0-1023一般被用作知名服务器的端口被预定,如www服务选择80端口,ftp服务选择21端口。


TCP/UDP

面向连接的套接字

  • 传输过程中数据不会丢失
  • 按顺序传输数据
  • 传输的过程不存在数据边界

面向消息的套接字

  • 强调快速传输而非顺序
  • 传输的数据可能丢失也可能损毁
  • 限制每次传输数据的大小
  • 传输的数据有数据边界

数据边界:比如要发送一百条消息,没有必要操心分几次传一次传多少。只要能到达就认为传输结束。

tcp比较像进货,不会太在意量,反正最后都要卖。
udp则像快递,每个货物大小重量限制,派送的时候择优先送,路上丢快递也不稀奇。


网络编程的基本类型和函数

1
2
3
SOCK_STREAM[流套接字] //TCP 面向连接、可靠的数据传输,适合传输大量的数据,不支持广播、多播
SOCK_DGRAM[数据包套接字] //UDP 无连接 支持广播、多播
SOCK_RAM[原始套接字] //可以读写内核没有处理的ip数据报,避开TCP/IP处理机制,被传送的数据包可以直接传送需要它的应用程序
  • 引用头文件winsock2.h
  • 导入ws2_32.lib库
  • window下socket变成都要先进行Winsock的初始化
函数名称 功能描述 适用范围
socket 创建套接字 面向连接的传输+面向无连接的传输
bind 套接字与本地ip地址和端口号的绑定 面向连接的传输+面向无连接的传输
connect 请求连接 面向连接的传输的客户机进程
listen 侦听连接请求 面向连接的传输的服务器进程
accept 接受连接请求 面向连接的传输的服务器进程
send 往已建立连接的套接字上发送数据 面向连接的传输
recv 从已建立连接的套接字上接收数据 面向连接的传输
sendto 在无连接的套接字上发送数据 主要用于无连接的传输
recvfrom 在无连接的套接字上接收数据 主要用于无连接的传输
close 关闭套接字 面向连接的传输+面向无连接的传输
1
2
3
4
5
6
7
8
9
10
typedef struct sockaddr {

#if (_WIN32_WINNT < 0x0600)
u_short sa_family;
#else
ADDRESS_FAMILY sa_family; // Address family.
#endif //(_WIN32_WINNT < 0x0600)

CHAR sa_data[14]; // Up to 14 bytes of direct address.
} SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;

对于sockaddr而言其实就俩成员,
一个无符号的短整型,也就是16位的地址类型
另一个14个char类型的数据,应该是ip+port

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct sockaddr_in {

#if(_WIN32_WINNT < 0x0600)
short sin_family;
#else //(_WIN32_WINNT < 0x0600)
ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)

USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

sockaddr_in在基础上多了一些。
16位的地址类型
16位的端口号
32位的ip地址
8字节填充

前者是给操作系统用,因为他把ip和地址混合了,而后者是做了区分。

对于没有引用头文件的时候想要查看定义就可以从文档下手,已知头文件的话就可以直接跳转到定义。


TCP

简易服务器

模型上都差不多的路数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include<stdio.h>
#include<stdlib.h>
#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")

int main(){
printf("TCP Server!\n");
//#ifdef 0
//*初始化网络库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(2, 2); //用winsock2头文件就写2
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0){
printf("WSAStartup errorNum = %d\n", GetLastError());
return err;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){
printf("LOBYTE errorNum = %d\n", GetLastError());
WSACleanup();
return -1;
}
//#endif // 0

//构造socket对象
SOCKET sockSer = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockSer){
printf("socket errorNum = %d\n", GetLastError());
return -1;
}

//填充参数
SOCKADDR_IN addrSer;
addrSer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSer.sin_family = AF_INET; //ipv4
addrSer.sin_port = htons(6000); //端口号

//绑定
if (SOCKET_ERROR == bind(sockSer, (SOCKADDR *)&addrSer, sizeof(SOCKADDR))){
printf("bind errorNum = %d\n", GetLastError());
return -1;
}

//监听
if(SOCKET_ERROR == listen(sockSer, 5)){
printf("listen errorNum = %d\n", GetLastError());
return -1;
}

SOCKADDR_IN addCli;
int len = sizeof(SOCKADDR);

while (true){
//分出子对象处理请求
printf("start\n"); //测试运行到哪
SOCKET sockConn = accept(sockSer, (SOCKADDR *)&addCli, &len);
printf("end\n"); //测试运行到哪
char sendBUf[100] = { 0 };
//sprintf_s(sendBUf, 100, "Welcome %s to China!", inet_ntoa(addCli.sin_addr));
sprintf_s(sendBUf, 100, "hello");
//收发数据
int iLen = send(sockConn, sendBUf, strlen(sendBUf),0);

char recvBuf[100] = { 0 };
iLen = recv(sockConn, recvBuf, 100, 0);

printf("recvBuf: %s", recvBuf);
//关闭子对象
closesocket(sockConn);
}

//服务结束关闭
closesocket(sockSer);
WSACleanup();

return 0;
}

简易客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<stdio.h>
#include<stdlib.h>
#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")


int main(){

printf("TCP Client\n");

//*初始化网络库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0){
printf("WSAStartup errorNum = %d\n", GetLastError());
return err;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){
printf("LOBYTE errorNum = %d\n", GetLastError());
WSACleanup();
return -1;
}

//构造socket对象
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockCli){
printf("socket errorNum = %d\n", GetLastError());
return -1;
}

//配置要连接的服务器
SOCKADDR_IN addrSer;
addrSer.sin_addr.S_un.S_addr = inet_addr("192.168.10.102"); //本机ip地址测试
addrSer.sin_family = AF_INET; //ipv4
addrSer.sin_port = htons(6000); //端口号


//连接服务器
if (SOCKET_ERROR == connect(sockCli, (SOCKADDR *)&addrSer, sizeof(SOCKADDR))){
printf("connect errorNum = %d\n", GetLastError());
return -1;
}

//收发数据,对比服务器,客户端应是先收后发
char recvBuf[100] = { 0 };
int iLen = recv(sockCli, recvBuf, 100, 0);
printf("recvBuf = %s\n", recvBuf);

const char sendBuf[100] = "hello";
iLen = send(sockCli, (char*)sendBuf, 100, 0);

//关闭套接字
closesocket(sockCli);
WSACleanup();

return 0;
}

本质上其实跟服务器差不多,像初始化网络库就肯定要套用的。

然后先回到之前服务器的debug目录右击管理员打开
然后vs debug跑现在的服务器

发现有回应了。

如果出现这些问题,需要注意服务器是否启动,或者客户端设置的服务器ip地址是否正确,如果套在本地虚拟网卡上,这个网卡又正好没启动也是无响应的状态。建议就直接配在连接的有线网卡或者无线网卡ip。


修改之前服务器连接的时候发送的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (true){
//分出子对象处理请求
printf("start\n"); //测试运行到哪
SOCKET sockConn = accept(sockSer, (SOCKADDR *)&addCli, &len);
printf("end\n"); //测试运行到哪
char sendBUf[100] = { 0 };
sprintf_s(sendBUf, 100, "Welcome %s to China!", inet_ntoa(addCli.sin_addr));
//sprintf_s(sendBUf, 100, "hello");
//收发数据
int iLen = send(sockConn, sendBUf, strlen(sendBUf),0);

char recvBuf[100] = { 0 };
iLen = recv(sockConn, recvBuf, 100, 0);

printf("recvBuf: %s", recvBuf);
//关闭子对象
closesocket(sockConn);
}

sprintf_s(sendBUf, 100, "Welcome %s to China!", inet_ntoa(addCli.sin_addr));
让他显示我们连接的ip地址。

可以看到成功显示了。


listen 5

1
2
3
4
if(SOCKET_ERROR == listen(sockSer, 5)){
printf("listen errorNum = %d\n", GetLastError());
return -1;
}

服务器监听的时候设置5的目的是为了,设置一个最大队列,让客户机有序的连接,并且不超过他的队列数。
说人话就是设置了瞬时访问人员数,等有人走了在放人进来。
另外像我们这种个人电脑,监听数设置太大电脑也无法承载。

自己搞测试的话可以在服务器listen下面加个sleep延时,然后快速打开超过五个客户端,看看是不是只有前面五个连上了,后面的要想连就只能等前面的结束了。

客户端加个暂停,避免超过5个连不上直接return -1结束程序。

1
2
3
4
5
6
7
//构造socket对象
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockCli){
printf("socket errorNum = %d\n", GetLastError());
system("pause");
return -1;
}

服务端加个延时和提示。

1
2
3
4
5
6
7
8
9
//监听
if(SOCKET_ERROR == listen(sockSer, 5)){
printf("listen errorNum = %d\n", GetLastError());
return -1;
}

printf("sleep start!\n");
Sleep(20000); //20s
printf("sleep end!\n");

然后先启动服务端,在快速打开多个客户端

额前面连接成功的结束的有点快自动就关闭了,
但是没关系,能看到总共还是成功连了5个客户端,第六个就报错了10061,也就是前面说过的问题,被计算机拒绝了。
然后其实就能想到,结束了访问之后在打开客户端连接只要没超过都是可以的。

end就闪了,但是好在服务端有提示,能看到除了之前快速打开的六个最后一个没连上,后面等前面五个都结束了,再去重新连接是能够连接上的。

这其实就挺像高并发的情况。


优化写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int MySocketRecv(int sock, char *buf, int dateSize){
//循环接收
int numRecvSoFar = 0; //目前接收到的数据
int numsRemainingToRecv = dateSize; //剩余要接受的数据
printf("enter MySocketRecv\n");

while (true){
int byteRead = recv(sock, &buf[numRecvSoFar], numsRemainingToRecv, 0);
printf("###bytesRead = %d, numsRecvSoFar = %d, numsRemainingToRecv = %d\m",
byteRead, numRecvSoFar, numsRemainingToRecv);

if (byteRead == numsRemainingToRecv){
return 0; //0则表示一次性接收完成
} else if (byteRead > 0){
numRecvSoFar += byteRead;
numsRemainingToRecv -= byteRead;
continue;
} else if ((byteRead < 0) && (errno == EAGAIN)){
continue;
} else{
return -1;
}
}

}

recv和send都可以通过相同路数。
其目的就是应对大型数据传输时,有特殊情况没全部传过来就断了,这样写可以分流缓冲。


UDP

前面写的其实都是tcp的操作,udp相对而言用的少。

看上去比tcp少了一些。


服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<iostream>
#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")


int main(){
//初始化网络库
printf("UDPServer!\n");
WORD wVersion;
WSADATA wsaData;
int err;

wVersion = MAKEWORD(2, 2); //使用winsock2,故此版本都选择2
err = WSAStartup(wVersion, &wsaData);
if (err != 0){
return err;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){
WSACleanup();
return -1;
}

//创建套接字 tcp采用流 udp采用报文
SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == sockSrv){
printf("socket errorNum = %d\n", GetLastError());
return -1;
}

//bind 分配地址和端口
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //地址族 h:hosts to n:net l:long
addrSrv.sin_family = AF_INET; //ipv4
addrSrv.sin_port = htons(6001); //端口号,0-1024保留

if (SOCKET_ERROR == bind(sockSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR_IN))){
printf("bind errorNum = %d\n",GetLastError());
return -1;
}

//阻塞等待接收数据
SOCKADDR_IN addrCli; //存储目的套接字的地址族
int len = sizeof(SOCKADDR_IN);

char recvBuf[100] = { 0 }; //收
char sendBuf[100] = { 0 }; //发

while (true){
recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR *) &addrCli, & len); //直接接收主机
std::cout << recvBuf << std::endl;

sprintf_s(sendBuf, 100, "Ack:%s", recvBuf);
sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrCli, len);
}

//关闭套接字
closesocket(sockSrv);
WSACleanup();

return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<iostream>
#include<WinSock2.h>

#pragma comment(lib,"ws2_32.lib")

int main(){
//初始化网络库
printf("UDPClient!\n");
WORD wVersion;
WSADATA wsaData;
int err;

wVersion = MAKEWORD(2, 2); //使用winsock2,故此版本都选择2
err = WSAStartup(wVersion, &wsaData);
if (err != 0){
return err;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){
WSACleanup();
return -1;
}

//创建套接字
SOCKET sockCli = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == sockCli){
printf("socket errorNum = %d\n", GetLastError());
return -1;
}

//填充地址和端口
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //地址族 本地回环地址
addrSrv.sin_family = AF_INET; //ipv4
addrSrv.sin_port = htons(6001); //端口号,0-1024保留

int len = sizeof(SOCKADDR_IN);
char sendBuf[100] = "hello";
char recvBuf[100] = { 0 };

//发送数据
sendto(sockCli, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR *)&addrSrv, len);

//接收数据
recvfrom(sockCli, recvBuf, 100, 0, (SOCKADDR *)&addrSrv, &len);
std::cout << recvBuf << std::endl;


//关闭套接字
closesocket(sockCli);
WSACleanup();

return 0;
}

其实很多东西都是相对应的,直接搬过来改一下就行了,所以感觉都没记住hh


测试

ok,连接成功。

多搞几个也没啥事,只不过客户端没加system("pause")去暂停,所以估计打开就是一闪而过了。


结语

也不好说到底要不要记这么详细,先凑合过吧,等有需要回头再看看。