我记得最开始接触网络程序是在我读大二的时候,当时我做的是一个聊天的程序,也不知道服务器和客户端的概念,在网上就是一顿找啊,才到自己能看懂的答案,但是只能两个程序能聊天。造成这样的原因是程序是阻塞的,然而那时我并不知道什么是阻塞,所以程序就搁置到那了。这个问题一直到我大四在一家音视频公司实习的时候才解决了,也是我对网络程序了解更深了一步。结合我自己对网络编程的经验,写了关于网络程序的篇章,一共有三篇。之所以想写下来,是因为想着我在学网络程序初期走了很多不必要的弯路,如果有正在想学网络程序的同僚,希望这篇文章能让你少走弯路。
废话不多说,开始听我洗脑。哈哈
所谓网络程序,就是写的程序能通过网络进行通信(就是交互数据),所以要想程序间能进行通信,至少要运行两个实例(其中,一个实例为服务器,一个为客户端)。一般来说,服务器实例只要一个,客户端实例可以多个。服务器和客户端的实例可以是同一个程序(该程序既是服务器,又是客户端,一般用于局域网),也可以是不同的程序(服务器一个程序,客户端一个程序)。
如何建立TCP链接?(相关代码都使用C++进行演示)
首先,个人建议先了解一下TCP协议,以及TCP链接建立的三次握手、断开的四次挥手。期间服务器和客户端状态变化建议了解比较好,这里不做讲解,感兴趣的可以去看看。
如果你不想了解那些原理,也没关系,并不影响编程。
服务端(只要记住六步):
1)创建套接字(socket)
2)绑定地址(bind)
3)监听(listen)
4)轮询等待客户端的接入(select | accept)
5)接收,发送消息(recv | send)
6)关闭(close)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#pragma comment(lib, “ws2_32.lib”)
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP0);
//2)绑定地址
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(8888);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
//3)监听
::listen(s, 5);
//4)轮询等待客户端的接入
int iLen = sizeof(SOCKADDR);
while (true) {
SOCKADDR_IN clientAddr;
SOCKET client = ::accept(s, (SOCKADDR *)&clientAddr, &iLen);
//5)发送消息
::send(client, "sever", 6, 0);
}
//6)关闭
closesocket(s);
return 0;
}
客户端(只要记住四步)
1)创建套接字(scoket)
2)连接服务器(connect)
3)接收,发送消息(recv | send)
4)关闭(close)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//2)连接
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(8888);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::connect(s, (sockaddr*)&addrServer, sizeof(addrServer));
//3)接收消息
char szBuf[1024] = { 0 };
int ret = recv(s, szBuf, 1024, 0);
szBuf[ret] = '\0';
printf("%s\n", szBuf);
//4)关闭
closesocket(s);
return 0;
}
上述代码是基于WinSocket编写的,visual studio编辑可直接运行。为了便于理解,很多异常情况没有判断。 看了上面客户端,和服务器的代码是不是觉得很容易?但是上诉程序只能保证一个客户端进行正常交互。为什么呢?原因就是服务器中的accept函数是阻塞函数,也就是当有新的客户端连接到服务器时,accept才会返回,否则一直处于等待状态。那么如何解决呢?这里可能很多同僚也知道,使用select模式轮询,或者epoll模式(Linux独有)。这里给大家科普一下,早期还没有select模式和epoll模式的时候,是如何解决阻塞的呢?是用线程处理的,当时最早的魔兽世界服务器据说是用线程处理的阻塞问题。因为accept,recv,send三个函数都是阻塞函数,所以服务端至少创建2倍的客户端+1的线程数量。这是在没有select和epoll模式的做法。但是线程多了,不仅消耗操作系统的资源,而且会让程序变得很复杂,不是很好维护。下面给出select模式下的,服务端和客户端代码。
服务端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
bool IsCanWrite(SOCKET s)
{
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(s, &writefds);
timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
int ret = select(FD_SETSIZE, NULL, &writefds, NULL, &timeout);
if (ret > 0 && FD_ISSET(s, &writefds)) {
return true;
}
return false;
}
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//2)绑定地址
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(8888);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
//3)监听
::listen(s, 5);
//4)轮询等待客户端的接入
SOCKET sMax = s;
int iLen = sizeof(SOCKADDR);
while (true) {
//判断是否有新的连接
fd_set fdRead;
FD_ZERO(&fdRead);
FD_SET(s, &fdRead);
int ret = -1;
timeval t_out = { 0, 0 };
ret = select(sMax + 1, &fdRead, NULL, NULL, &t_out);
if (ret > 0) {
if (FD_ISSET(s, &fdRead)) {
SOCKADDR_IN clientAddr;
SOCKET client = ::accept(s, (SOCKADDR *)&clientAddr, &iLen);
//5)发送消息
//判断套接字是否可写
if (IsCanWrite(client)) {
::send(client, "sever", 6, 0);
}
}
}
//防止独占CPU
Sleep(10);
}
//6)关闭
closesocket(s);
return 0;
}
客户端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
bool IsCanRead(SOCKET s)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(s, &readfds);
timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
int ret = select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(s, &readfds)) {
return true;
}
return false;
}
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//2)连接
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(8888);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::connect(s, (sockaddr*)&addrServer, sizeof(addrServer));
//3)接收消息
while (true) {
//判断套接字是否可读
if (IsCanRead(s)) {
char szBuf[1024] = { 0 };
int ret = recv(s, szBuf, 1024, 0);
szBuf[ret] = '\0';
printf("%s\n", szBuf);
}
//防止独占CPU
Sleep(10);
}
//4)关闭
closesocket(s);
return 0;
}
上述就是阻塞模式和非阻塞模式的代码,这里非阻塞模式只给了select模型,至于epoll模型,可以自行科普(epoll和select模型是有区别,根据不同的情况选择不同的模式,这里不做说明)。
如何建立UDP链接?(相关代码都使用C++进行演示)
还是个人建议先了解一下UDP协议,相对于TCP协议比较而言,UDP协议简单的多。
如果你不想了解那些原理,也没关系,并不影响编程。
服务端(只要记住四步):
1)创建套接字(socket)
2)绑定地址(bind)
3)接收,发送消息(recv | send)
4)关闭(close)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//2)绑定地址
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(9999);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
::bind(s, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
//3)接收消息
while (true) {
sockaddr_in addr;
int addr_len = sizeof(SOCKADDR);
char szBuf[1024] = { 0 };
int ret = recvfrom(s, szBuf, 1024, 0, (sockaddr*)&addr, &addr_len);
if (ret > 0) {
szBuf[ret] = '\0';
printf("%s\n", szBuf);
}
//防止CPU空转
Sleep(10);
}
//4)关闭
closesocket(s);
return 0;
}
客户端(只要记住三步):
1)创建套接字(socket)
2)接收,发送消息(recv | send)
3)关闭(close)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
//检测版本号
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
::WSAStartup(wVersionRequested, &wsaData);
//1)创建套接字
SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//2)发送消息
SOCKADDR_IN addrServer;
int addr_len = sizeof(SOCKADDR);
addrServer.sin_family = AF_INET;
addrServer.sin_port = ::htons(9999);
addrServer.sin_addr.S_un.S_addr = ::inet_addr("127.0.0.1");
sendto(s, "client", 6, 0, (sockaddr*)&addrServer, addr_len);
//3)关闭
closesocket(s);
return 0;
}
上述就是UDP连接的相关代码,为了便于理解,有些异常情况没有判断,在实战项目中,注意点就行。下面说一下TCP和UDP链接的特点,网同僚在以后开发中自己进行选取。
TCP链接:需要建立连接,稳定(超时重传机制),数据不容易丢失,流式数据;
UDP链接:无需建立链接,不稳定,数据可能会丢,报文数据
套接字属性?
其实套接字本身是还有很多属性的,如果仅仅以为掌握上述的select代码就算掌握的话,那你可就太天真了。上述本人给的select模式代码,默认是非阻塞的;其实也可以把select设置成超时阻塞模式,这就需要知道套接字属性字段了。下面我例举部分属性字段,主要目的是让知道套接字有属性就行,具体哪些可以网上查阅。
//下面是winsocket的属性,主要是命名不一样,Linux下也有,可能是别的命名方式
FIONBIO //阻塞模式
SO_LINGER //linger算法(节约流量,不知道的网上科普)
SO_SNDBUF //协议栈(这个概念不是很清楚的,先放一放,或者去科普,因为随着你写的越多,你就慢慢明白了)的发送缓冲区大小
SO_RCVBUF //协议栈的接收缓冲区大小
SO_SNDTIMEO //发送超时
SO_RCVTIMEO //接收超时
SO_REUSEADDR //重用地址
TCP_NODELAY //延时发送(如果开启,就可能会产生黏包)
//等等,还有很多字段,但是常用(本人常用)的就这么几个
到这里,关于套接字,TCP和UDP操作,基本差不多了,好像也就只这些。所以,不用把网络编程想的太困难。最后,附上本人套接字的封装代码,有需要的自行下载。本人只实现了C/C++语言(Windows/Linux)和PHP语言两个版本,其实都差不多,懂了原理各种语言都一样,都是API的名字不一样罢了。好吧,不说了,祝早日成功。
链接:https://pan.baidu.com/s/1IbZBsQxR2yUSs06aK5AmXw
提取码:jzt1