前言
所谓套接字(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[流套接字] SOCK_DGRAM[数据包套接字] SOCK_RAM[原始套接字]
|
- 引用头文件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; #endif
CHAR sa_data[14]; } 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 ADDRESS_FAMILY sin_family; #endif
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");
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 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; 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, "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 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"); addrSer.sin_family = AF_INET; 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)); 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 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); 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; } 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); err = WSAStartup(wVersion, &wsaData); if (err != 0){ return err; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){ WSACleanup(); return -1; }
SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0); if (INVALID_SOCKET == sockSrv){ printf("socket errorNum = %d\n", GetLastError()); return -1; }
SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(6001);
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); 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; addrSrv.sin_port = htons(6001);
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")
去暂停,所以估计打开就是一闪而过了。
结语
也不好说到底要不要记这么详细,先凑合过吧,等有需要回头再看看。