本文将介绍
①Socket是什么
②Socket与TCP/IP的关系
③Socket编程中具体接口的用法 Bind() Connect() Accept() etc..
④Socket编程示例(VS2015)下载示例
1. Socket(套接字)是什么
Socket(套接字)是指通讯设备的连接端点,不用于网口、USB端口这样的端点是实质的,套接字是虚拟的,包含了通讯所需的信息的端口。套接字有很多种,本文主要介绍Internet套接字。Internet套接字包含网络通讯必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远程主机的IP地址,远程主机进程的协议端口。
Internet套接字有以下几种:
SOCK_STREAM:流式套接字,提供面向连接的稳定数据流传输,即TCP协议。
SOCK_DGRAM:数据报套接字,使用不连续不可靠的数据包连接,即UDP协议。
SOCK_SEQPACKET:有序分组套接字,提供连续可靠的数据包连接。
SOCK_RAW:原始套接字,允许对底层协议直接访问,如IP/ICMP协议等。
SOCK_RDM:提供可靠的数据包连接。
SOCK_PACKET:与网络驱动程序直接通信。
2. Socket与TCP/IP关系
关于TCP/IP
相信很多人都听过Socket(套接字)和TCP/IP,两者总是一起出现,好似是同一个东西,但我们都知道它们是不一样的,那究竟两者有什么关系Socket包含了连接使用的协议,这个协议指的就是TCP/IP协议族。TCP/IP协议族不只是TCP和IP,它可以详细分为四层,每层上有不同的协议,对应ISO-OSI七层网络模型。
OSI七层网络模型 | TCP/IP四层概念模型 | 对应网络协议 |
应用层(Application) | 应用层 | HTTP、TFTP, FTP, NFS, WAIS、SMTP |
表示层(Presentation) | Telnet, Rlogin, SNMP, Gopher | |
会话层(Session) | SMTP, DNS | |
传输层(Transport) | 传输层 | TCP, UDP |
网络层(Network) | 网络层 | IP, ICMP, ARP, RARP, AKP, UUCP |
数据链路层(Data Link) |
数据链路层
| FDDI, Ethernet, Arpanet, PDN, SLIP, PPP |
物理层(Physical) | IEEE 802.1A, IEEE 802.2到IEEE 802.11 |
TCP/IP内容非常庞大,此处不展开讲解,后面文章会详细讲解TCP/IP协议族。此处下载《TCP/IP协议详解 - 第五版》 点击打开链接
TCP/IP协议族之间的抽象关系可用下图表示
那么Socket在哪里?其实Socket是一套API函数,这套函数实现了上面的通讯过程。
固然我们可以按照上图所示,把每个协议功能都实现,但这样太繁琐了,软件不易于改动扩展。于是UNIX BSD就发明了socket,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。它对内封装了TCP/IP协议,对外提供一套统一的接口,我们只需调用接口即可实现TCP/IP协议的通讯过程。看下图示意

<例子>TCP通讯的Socket编程抽象过程
Socket编程中最常见的是TCP编程,下面以TCP协议为例,简述客户端和服务端的Socket函数调用过程,其他协议编程类似。

客户端过程:
1. Socket()建立Soocket套接字,指定套接字类型。
2. Connect()用IP和Port建立连接,指定协议族内容,连接方法等。TCP连接有三次握手过程,封装在Ws2_32.dll内部。函数只在客户端中使用。
3. Write()连接成功,向服务器发送数据,数据以ANSI形式发送。
4. Read()读取服务器返回数据,例如发送命令后处理结果OK/NG。
5. Close()客户端关闭连接,TCP断开有四次挥手过程,封装在Ws2_32.dll内部。
服务器过程:
1. Socket()建立Soocket套接字,指定套接字类型。
2. Bind()将套接字绑定到指定IP和Port上,只在服务器中使用。
3. Listen()服务器监听客户端的连接,一个服务器可以监听多个客户端。
4. Accept()服务器同意客户端的连接,与客户端的Connect()函数对应,这两个配套函数分别在各自的程序中被成功调用后就可以收发数据了。
5. Read()读取来自客户端的数据。
6. Write()处理完客户端数据后,(如有需要)向客户端返回数据。
7. Read()客户端调用了Close() 后会发送断开信息,服务器回应断开该链接。
8. Close()关闭服务器。
实际使用中,服务器不随着客户端Close() 而关闭,仅仅断开对应的连接(Shutdown)。服务器不断监听客户端的连接请求,当有连接请求时建立双向连接,客户端向服务器交互信息。客户端关闭时断开连接,服务器继续监听连接请求。
3. Socket编程中具体接口的用法
Socket的函数很多,Windows中头文件是Winsock2.h,下面介绍经常使用的几个。
Socket()接口
SOCKETWSAAPI socket(
_In_ int af,
_In_ int type,
_In_ int protocol
);
功能:创建一个套接字
① af 指地址族,通常使用
AF_INET: 采用IPV地址
AF_INET6: 采用IPV6地址
AF_LOCAL/AF_UNIX:使用绝对路径名
AF_ROUTE: 路由套接字
AF_KEY: 秘钥套接字
补充:AF_是地址族(Address Family),PF_是协议族(Protocol Family),通常它们是一样的,例如AF_INET与PF_INET都表示IPV4地址,但在Unix中个别字段有差异。历史上曾想以单个协议族表示多个地址族,最终没成功。
② type表示socket类型,通常使用
SOCK_STREAM: 流式套接字,提供面向连接的稳定数据流传输,即TCP协议。
SOCK_DGRAM: 数据报套接字,使用不连续不可靠的数据包连接,即UDP协议。
SOCK_SEQPACKET: 有序分组套接字,提供连续可靠的数据包连接。
SOCK_RAW: 原始套接字,允许对底层协议直接访问,如IP/ICMP协议等。
SOCK_RDM: 提供可靠的数据包连接。
SOCK_PACKET: 与网络驱动程序直接通信。
③ protocol表示具体的协议,通常使用
IPPROTO_TCP: tcp传输协议
IPPROTO_UDP: udp传输协议
IPPROTO_SCTP: sctp传输协议
protocol一般选0,表示使用family和type组合所选择的协议。常用组合有
AF_INET+SOCK_STREAM = IPPROTO_TCP/IPPROTO_SCTP
AF_INET+SOCK_DGRAM = IPPROTO_UDP
AF_INET+SOCK_SEQPACKET = IPPROTO_SCTP
AF_INET+SOCK_RAW = IPv4
但不是所有的组合都有意义
Bind()接口
int WSAAPIbind(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const structsockaddr FAR * name,
_In_ int namelen
);
功能:将本地协议地址绑定一个Socket,Socket将使用这个地址进行通信。
① SOCKETs表示socket()函数创建的socket描述符
② name表示具体的地址
//SOCKADDR_IN结构体设置IPv4地址
typedef struct sockaddr_in{
ADDRESS_FAMILYsin_family; // AF_INET
USHORTsin_port; // 设置端口
IN_ADDRsin_addr; // 设置IP地址
CHARsin_zero[8];
}
//SOCKADDR_IN6结构体设置IPv6地址
typedef struct sockaddr_in6{
ADDRESS_FAMILYsin6_family; // AF_INET6.
USHORTsin6_port; // 设置端口.
ULONG sin6_flowinfo; // IPv6 flow information.
IN6_ADDRsin6_addr; // IPv6 地址.
union{
ULONGsin6_scope_id; // Set of interfacesfor a scope.
SCOPE_IDsin6_scope_struct;
};
}
③ namelen表示结构体地址长度,用sizeof()可求得。
Listen()接口
int WSAAPIlisten(
_In_ SOCKET s,
_In_ int backlog
);功能:服务器调用,将一个未连接的套接字变为被动套接字,告知套接字监听是否有连接到来。
① SOCKETs表示socket()函数创建的socket描述符
② backlog指定最大的连接客户端数量
Connect()接口
int WSAAPIconnect(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const structsockaddr FAR * name,
_In_ int namelen
);功能:客户端调用,用于客户端与服务器连接。参数含义与Bind()相似。
① SOCKETs表示socket()函数创建的socket描述符
② name表示具体的地址
SOCKADDR_IN结构体设置IPv4地址
SOCKADDR_IN6结构体设置IPv6地址
③ namelen表示结构体地址长度,用sizeof()可求得。
Accept()接口
SOCKETWSAAPI accept(
_In_ SOCKET s,
_Out_writes_bytes_opt_(*addrlen) structsockaddr FAR * addr,
_Inout_opt_ int FAR * addrlen
);
功能:服务器调用,客户端发出Connect()请求后,会形成Accept连接队列,服务器从连接队列中取出一个连接,通过该函数生成新的Socket描述符,这个新的描述符将被拿来供客户端与服务器交互数据。
① SOCKETs表示socket()函数创建的socket描述符
② addr(输出参数),表示新的套接字描述符的地址信息
③ addrlen(输出参数),表示新的套接字描述符的地址长度
④ 函数返回SOCKET s,返回一个新的套接字,这个套接字用在下面的发送/接收消息。
Write/Read 发送数据&接受数据
Send()函数
int WSAAPIsend(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags
);
功能:客户端与服务器之间发送数据
① SOCKETs表示accept()函数返回的socket描述符
② buf表示发送的字节内容
③ len表示发送内容的字节长度
④ flags标识位,一般置0,但某些场景需要设置标识
Recv()函数
int WSAAPIrecv(
_In_ SOCKET s,
_Out_writes_bytes_to_(len, return)__out_data_source(NETWORK) char FAR * buf,
_In_ int len,
_In_ int flags
);
① SOCKETs表示accept()函数返回的socket描述符
② buf表示接收的字节内容
③ len表示接受内容的buffer长度,必须比接收的内容大
④ flags 标识位,一般置0,但某些场景需要设置标识
关于标识位字段:
MSG_PEEK,可以用于recv、recvfrom查看已可读取的数据,读取完成后不丢弃数据,Recv()可用
MSG_DONTROUTE,目的主机在某个直连的本地网络上,不经过路由表查找,Send()可用
MSG_OOB,发送或接收带外数据,Send()、Recv()可用
MSG_WAITALL,告诉内核不要在未读完指定的字节前返回,Recv()可用
MSG_DONTWAIT,仅本操作非阻塞,Recv()、send()可用,Windows系统不支持
关于带外数据介绍:
http://blog.163.com/clevertanglei900@126/blog/static/111352259201122142545411/
Sendto()函数
int WSAAPIsendto(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags,
_In_reads_bytes_(tolen) const structsockaddr FAR * to,
_In_ int tolen
);
功能:用于UDP中发送报文,返回值表示发送的数据数量。
① SOCKETs表示accept()函数返回的socket描述符
② buf表示发送的字节内容
③ len 表示发送内容的字节长度
④ flags 标识位,一般置0,但某些场景需要设置标识
⑤ to发送数据到指定地址,表示数据报将发送何处(UDP)或者与之建立连接到协议地址(TCP)
⑥ tolen发送表的长度
Recvform()函数
int WSAAPIrecvfrom(
_In_ SOCKET s,
_Out_writes_bytes_to_(len, return)__out_data_source(NETWORK) char FAR * buf,
_In_ int len,
_In_ int flags,
_Out_writes_bytes_to_opt_(*fromlen,*fromlen) struct sockaddr FAR * from,
_Inout_opt_ int FAR * fromlen
);
功能:用于UDP中接收报文,返回值表示接收到数据数量
① SOCKETs表示accept()函数返回的socket描述符
② buf表示接收的字节内容
③ len表示接受内容的buffer长度,必须比接收的内容大
④ flags 标识位,一般置0,但某些场景需要设置标识
⑤ from输出参数,表明了是谁发送了数据报(UDP)或者是谁发起了连接(TCP)
⑥ from参数表的长度,必须比收到的参数内容大
Close()接口
int WSAAPIclosesocket(
_In_ SOCKET s
);
功能:用于关闭套接字。执行完关闭后,套接字将不再允许任何读写操作。
① SOCKETs表示需关闭的套接字
Shutdwown()
int WSAAPIshutdown(
_In_ SOCKET s,
_In_ int how
);
功能:支持更高级的关闭操作, 利用shutdown,可以实现tcp连接半关闭
① SOCKET s表示需关闭的套接字
② how 标识符
SD_RECEIVE 不允许数据接收
SD_SEND 不允许数据发送
SD_BOTH 不允许接受或发送,同Close()
Close()接口
int WSAAPIclosesocket(
_In_ SOCKET s
);
功能:用于关闭套接字。执行完关闭后,套接字将不再允许任何读写操作
① SOCKETs表示需关闭的套接字
Shutdwown()
int WSAAPIshutdown(
_In_ SOCKET s,
_In_ int how
);
功能:支持更高级的关闭操作, 利用shutdown,可以实现tcp连接半关闭
① SOCKET s表示需关闭的套接字
② how 标识符
SD_RECEIVE 不允许数据接收
SD_SEND 不允许数据发送
SD_BOTH 不允许接受或发送,同Close()
获取地址协议接口
Getsockname()函数
int WSAAPIgetsockname(
_In_ SOCKET s,
_Out_writes_bytes_to_(*namelen,*namelen)struct sockaddr FAR * name,
_Inout_ int FAR * namelen
);
功能:获取与socket相关的本地协议地址,只有在连接建立以后才调用,成功返回0,失败返回-1。
① SOCKET s表示需操作的socket描述符
② name 输出参数,返回的本地地址信息,IP和Port等
③ namelen地址结构体的长度,必须比返回信息表长
Getpeername()函数
int WSAAPIgetpeername(
_In_SOCKET s,
_Out_writes_bytes_to_(*namelen,*namelen)struct sockaddr FAR * name,
_Inout_ int FAR * namelen
);
功能:获取与socket相关的对端协议地址,只有在连接建立以后才调用,成功返回0,失败返回-1。
① SOCKET s表示需操作的socket描述符
② name 输出参数,返回的对端地址信息,IP和Port等
③ namelen地址结构体的长度,必须比返回信息表长
4. Socket编程示例
本示例使用VS2015编写,用ANSI字符集,Unicode字符集需要宽转窄字符,搭建相互通讯的TCP客户端和服务器。TCP通讯与UPD通讯在Socket中实现方法相同,选择不同的标识字段可切换。
============= 客户端 =============
// 输入IP和Port建立连接
BOOL Connect(CString strIP, int iPort)
{
WORD wVersionRequested;
WSADATA wsaData;
// 向操作系统说明,需要用2.2版本的Socket,绑定相应的库
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) == 0)
{
return TRUE;
}
else
{
// 解除Socket库的绑定,释放资源
WSACleanup();
return FALSE;
}
const char* pstr = strIP;
// 将IP和Port转为sockaddr结构体
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
client.sin_family = AF_INET; // IPV4
client.sin_port = htons(iPort); // 设置端口
inet_pton(client.sin_family, pstr, &(client.sin_addr)); // 设置IP
// 新建一个socket
m_sSocket = socket(AF_INET, SOCK_STREAM, 0);
if (m_sSocket == INVALID_SOCKET)
{
error = WSAGetLastError();
return FALSE;
}
// 连接
if (connect(m_sSocket, (LPSOCKADDR)&client, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
error = WSAGetLastError();
closesocket(m_sSocket);
m_sSocket = NULL;
return FALSE;
}
m_bCreated = TRUE;
return TRUE;
}
// 输入信息内容和长度,向服务器发送信息
int SendClient(char* data, int length)
{
int nRet = send(m_sSocket, data, length, 0);
if (nRet == SOCKET_ERROR)
{
error = WSAGetLastError();
}
return nRet;
}
// 关闭连接
void Close()
{
shutdown(m_sSocket, SD_RECEIVE);
closesocket(m_sSocket);
m_sSocket = NULL;
m_bCreated = FALSE;
WSACleanup();
}
============= 服务器 =============
服务器比客户端复杂,需要开启线程监听客户端请求,连接成功后保持与客户端的信息互动。
// 用IP和Port创建服务器
BOOL mysocket::Create(CString strIP, int iPort)
{
WORD wVersionRequested;
WSADATA wsaData;
// 向操作系统说明,需要用2.2版本的Socket,绑定相应的库
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaData) == 0)
{
return TRUE;
}
else
{
// 解除Socket库的绑定,释放资源
WSACleanup();
return FALSE;
}
const char* pstr = strIP;
m_sockMain = socket(AF_INET, SOCK_STREAM, 0); // IPV4 & TCP
if (m_sockMain == INVALID_SOCKET) // WASStartup
return FALSE;
SOCKADDR_IN addrRobot;
memset(&addrRobot, 0, sizeof(addrRobot));
addrRobot.sin_family = AF_INET; // Ipv4
addrRobot.sin_port = htons(iPort);
// inet_pton 将ip绑定到addrRobot.sin_addr
INT iRe = inet_pton(addrRobot.sin_family, pstr, &(addrRobot.sin_addr));
if (iRe < 1)
return FALSE;
if (SOCKET_ERROR == bind(m_sockMain, (LPSOCKADDR)&addrRobot, sizeof(addrRobot)))
{
closesocket(m_sockMain);
m_sockMain = INVALID_SOCKET;
return FALSE;
}
if (SOCKET_ERROR == listen(m_sockMain, 5))
{
closesocket(m_sockMain);
m_sockMain = INVALID_SOCKET;
return FALSE;
}
SetEvent(m_hEventListenning);
CreateThread(NULL, 0, ThreadListen, this, 0, &m_ulListen);
return TRUE;
}
// 监听使用线程,监听到客户端连接后(accept())),开启子线程接收客户端的信息。这样可继续监听其他客户端的连接。
DWORD WINAPI ThreadClient(LPVOID pParam);
DWORD WINAPI ThreadListen(LPVOID pParam);
DWORD WINAPI ThreadListen(LPVOID pParam)
{
mysocket* pServer = (mysocket*)pParam;
if (pServer == NULL)
return 0;
closesocket(pServer->m_sockClient);
pServer->m_sockClient = INVALID_SOCKET;
SOCKADDR_IN addrRobot;
int len = sizeof(SOCKADDR_IN);
while (!pServer->IsStopListenning())
{
if (!pServer->HasClient())
{
pServer->m_sockClient = accept(pServer->m_sockMain, (LPSOCKADDR)&addrRobot, &len);
if (INVALID_SOCKET == pServer->m_sockClient)
{
break;
}
// 与客户端交互
SetEvent(pServer->m_hEventClient);
CreateThread(NULL, 0, ThreadClient, pServer, 0, &pServer->m_ulClient);
Sleep(100);
}
Sleep(50);
}
pServer->Close();
return 0;
}
DWORD WINAPI ThreadClient(LPVOID pParam)
{
mysocket* pServer = (mysocket*)pParam;
if (pServer == NULL)
return 0;
pServer->Interaction();
return 0;
}
// 交互
void mysocket::Interaction()
{
int iRecv;
char szRecv[MAX_PATH * 2];
memset(szRecv, 0, sizeof(szRecv));
while (HasClient())
{
if (m_sockClient == INVALID_SOCKET)
{
break;
}
memset(szRecv, 0, sizeof(szRecv));
// 接收
iRecv = recv(m_sockClient, szRecv, sizeof(szRecv), 0);
if (0 == iRecv || SOCKET_ERROR == iRecv)
{
break;
}
m_strRecv = szRecv;
//...//
// 处理客户端数据 if(strRecv == "RUN") then...
// 处理完毕返回消息给客户端 SendString("OK")
//...//
}
closesocket(m_sockClient);
ResetEvent(m_hEventClient);
}
// 客户端断开连接,会退出ThreadClient线程,监听线程仍继续。直到程序结束,关闭服务器。
// 关闭服务器
void mysocket::Close()
{
closesocket(m_sockMain);
closesocket(m_sockClient);
ResetEvent(m_hEventClient);
Sleep(50);
ResetEvent(m_hEventListenning);
Sleep(50);
WSACleanup();
m_sockClient = INVALID_SOCKET;
m_sockMain = INVALID_SOCKET;
}
具体实现工程请下载示例
332



被折叠的 条评论
为什么被折叠?



