为了各种面试重新来复习一下tcp/ip,在初次学完之后也一直就只是有一个大概的概念,具体到里面好多个网络模型什么的已经忘得差不多了,正好现在重新捡起来。
先回顾一下标准c/s模型的server流程。
1. 打开网络库及版本校验
2. 创建socket
3. 绑定地址端口号
4. 设置监听状态
5. 接受socket
6. 收发消息
7. 关闭服务
其实这些流程映射到代码中非常简单,基本就是一个过程对应了一个函数。
1.打开网络库及版本校验
- 具体到操作就是WSAStartup这个函数,与之对应的还有WSACleanup用于关闭网络库。
- 有两个参数, 一个版本参数使用宏创建WORD变量,另一个参数是WSADATA 结构体指针作为返回值,执行结束后可以查看这个结构体,里面包含网络库打开版本,以及支持最高版本等信息。
WORD wVersion = MAKEWORD(2, 2);
WSADATA wsadata;
WSAStartup(wVersion, &wsadata);
if (2 != HIBYTE(wsadata.wVersion) || 2 != LOBYTE(wsadata.wVersion))
{
printf("wrong version");
WSACleanup();
return 0;
}
2.创建socket
- socket函数,第一个参数描述为The address family specification。建议查看微软官方文档socket。第二个参数意思是基于数据流,第三个参数说明协议类型。
- 执行成功返回一个可用的socket,执行失败返回INVALID_SOCKET ,可以通过WSAGetLastError获取错误码。
SOCKET socketS = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);
3.绑定地址端口号
- 主要是需要自己创建sockaddr结构体,微软提供了另一个大小一致sockaddr_in的结构体方便我们进行内容填写。只需要注意在传参时进行类型转换就行。
- 结构体内容很简单,family和创建socket第一个参数一致,还需要端口号和ip地址,这里使用“127.0.0.1”本地回环。
(结构体这一部分直接在环境中查看源代码定义会更加清楚。)
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = 12345;
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
bind(socketS, (const struct sockaddr*)(&si),sizeof(struct sockaddr));
4.设置监听状态
- 就一个函数,没什么好说的。第一个参数是服务器socket,第二个参数是待处理连接队列的最大长度。一般使用SOMAXCONN这个宏。具体看官方文档listen
listen(socketS, SOMAXCONN);
5.接受socket
- 返回值是接收到的客户端socket,后两个参数分别是sockaddr结构体指针和指向该结构体大小数字的指针,用来存放接收到的客户端socket的一些数据,也可以填NULL不主动保存这些数据,因为在我们的使用中只需要封装好的socket就可以了,但是如果想要使用的话也可以通过getpeername函数来获取。
- 阻塞操作,有多少个客户端就要执行多少次accept,但是没有新的socket链接的话就不会向下进行。
SOCKET socketC = accept(socketS, NULL, NULL);
6.收发消息
- recv函数其实是读取读取缓冲区消息,放到函数参数的中buff中,是tcp/ip协议完成了通信的过程。
- send函数也是一样,将发送内容写入到发送缓冲区。
- recv是阻塞操作,返回值是收到的字节数,如果是0则表示客户端以下线,如果客户端未发送数据则一直阻塞。(客户端在下线会进行四次挥手,服务器就会释放客户端socket资源,所以recv直接返回0。)
- tips:测试表示客户端正常连接并下线之后,使用recv返回值确实是0。但若是客户端被强制关闭会返回SOCKET_ERROR,应当作出处理。send函数并不阻塞,甚至在客户端已下线的情况下还能执行一次,并且返回发送字节数。但是第二次会返回SOCKET_ERROR,通过WSAGetLastError查询到错误码为10053,原因大概是第一次执行看似成功其实没有,人后在第二次执行反馈出发送超时的错误码。
char* buff[1500] = { 0 };
int res = recv(socketC, buff, 1499, 0);
printf("len: %d\trecive: %s", res,buff);
res = send(socketC, "hello", sizeof("hello"), 0);
7.关闭服务
- 使用结束需要关闭socket释放资源,也是避免内存泄漏。
- 这可以封装一下,在开启服务器的过程中也可能会有各种各样的问题,每一步出现问题的时候都可能需要执行这些步骤中的一个或多个。
closesocket(socketC); //客户端socket
closesocket(socketS); //服务器socket
WSACleanup(); //关闭网络库
接下来是客户端内容,相比于服务器,客户端的流程就简单多了。
1. 打开网络库及版本校验
2. 创建socket
3. 连接到服务器
4. 收发消息
5. 关闭服务
1.打开网络库及版本校验
- 和服务器一样的内容。
2.创建socket
- 创建服务器socket,流程也和服务器一样。
3.连接到服务器
- 连接到服务器并把服务器socket和服务器信息绑定。
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = 12345;
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
connect(socketS, (const struct sockaddr*)(&si), sizeof(struct sockaddr));
4.收发消息
- 也差不多和服务器相同,一边发另一边收。
5.结束服务
- 关闭socket,清理网络库。
至此一个非常基础的网络模型就建立完成了,更改其中的ip地址还可以实现在局域网中的通信,当然其中收发消息的代码也仅需要适当的修改便可以进行简单的命令行聊天。
技术类的学习开头看起来总是枯燥的,硬件离不开点灯,软件离不开helloworld。但是我相信激动人心的地方并不仅仅在于一颗亮起的灯,或者在黑乎乎的窗口下一行的helloworld,在于我们通过自己坚持下来的努力让我们清楚的知道,自己能够操控自己眼前的这个机器,这一个高度集成的电路板,这一个融合了上亿个晶体管的芯片,他刚刚遵循我们的指令做到了我们想要让他做到的事情。
我很喜欢生活大爆炸中的一个片段,四位科学家将自己的房间接入了互联网,随后可以看到一个来自中国四川某地的用户在他的家中发出信号,经过本地的ISP,进入互联网,穿越太平洋,来到美国加州控制他们家中的一个台灯的亮灭。
这在外人开起来好像是一件相当无趣且没有意义的事情,但是我相信每一个有技术的人在能够做到这一步的情况下,在展现出成功的信号的时候,都是心潮澎湃的。