目录
何为socket
Socket本身也是进程间通信方式的一种。但是与其他的方式不同,socket可以用于不同主机间的进程通信,这得益于它的创建方式和工作流程。在之前介绍TCP和UDP的文章中,端口作为首部中的一部分被提及,但并未进行详细的描述。实际上端口代表的就是一个进程,而一些特定的进程都有其固定的端口号,如DNS使用的默认端口号为53、通过HTTP向Web服务器请求网页的默认端口号为80等。而套接字的作用就是将端口与进程绑定,从而使发往指定端口的数据能够正确的被相应的进程接收。因此socket的本质就是传输层对应用层提供的接口,通常服务器端与客户端所使用的socket是不同的。在使用TCP时,服务器端会一直存在一个用于监听是否有客户端发起新的连接的socket,这种socket被称之为master socket;当确认需要建立连接时,双方便会各自新建一个用于传输数据的socket,这种socket则被称之为connected socket。在使用UDP时,不会建立真实的连接,而只是在收发数据时指定相应的地址和端口。
使用socket
在进程使用socket进行各种操作时,就需要用到其暴露给应用层的各种接口,即socket API。在不同操作系统环境下使用不同语言操作socket的方法都各有不同,在这里主要介绍使用C/C++时的socket API。而通过TCP和UDP进行通信时,使用socket的流程也是不一样的。
一、C/C++ socket API
在Linux和Windows环境下,使用socket时需要引入的头文件各不相同,使用的流程、API、数据类型和一些结构体的结构也有所不同,在具体使用时需要深入了解,这里只讨论在Windows环境下对Windows Socket 2的使用。在使用之前,需要在引入相关的静态库 (Ws2_32.lib,或是动态载入Ws2_32.ddl) 后再引入头文件winsock2.h。在操作socket之前,需要调用其中的WSAStartUp() 进行初始化,而在使用完Windows Socket 2之后,需要调用WSACleanup()进行收尾。想要了解更多Windows Socket 2中API的具体使用方法可以阅读官方文档,以下部分仅节选了几个常用的API进行简单的描述:
1. 创建socket
/*
方法
*/
// 创建socket
SOCKET WSAAPI socket(
[in] int af, // address family,指定地址簇的枚举
[in] int type, // 指定socket类型的枚举
[in] int protocol // 指定使用协议的枚举
); // 若成功则返回描述符,否则返回INVALID_SOCKET
// 绑定 (无强制规定但通常只有客户端使用)
// 由于服务器端的进程通常使用固定端口,因此通常必须绑定
// 由于客户端的进程通常使用随机端口,因此通常不会绑定,否则需要确认当前端口是否已被占用
int WSAAPI bind(
[in] SOCKET s, // 未绑定socket的描述符
[in] const sockaddr *name, // sockaddr结构体的位置,对不同地址簇其含义不同
[in] int namelen // sockaddr结构体的长度
); // 若成功则返回0,否则返回SOCKET_ERROR
/*
简单示例
*/
// 声明变量
SOCKET sock; // 声明一个socket
struct sockaddr_in saServer; // 声明一个sockaddr_in结构体
hostent* localHost; // 声明一个指向本地主机的指针
char* localIP; // 声明一个指向本地主机IP地址的指针
// 创建一个在TCP中使用IPv4地址簇的socket
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 获取本地主机信息
localHost = gethostbyname("");
localIP = inet_ntoa (*(struct in_addr *)*localHost->h_addr_list);
// 更新sockaddr_in结构体
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(localIP);
saServer.sin_port = htons(5150);
// 使用sockaddr_in结构体中的内容绑定socket
// 传参时将sockaddr_in结构体类型转换成sockaddr结构体
bind(sock, (SOCKADDR*) & saServer, sizeof (saServer));
2. 建立连接(仅限TCP)
/*
方法
*/
// 监听 (服务器端)
int WSAAPI listen(
[in] SOCKET s, // 已绑定但未连接的socket的描述符
[in] int backlog // 挂起连接的队列的最大长度
); // 若成功则返回0,否则返回SOCKET_ERROR
// 同意连接 (服务器端)
SOCKET WSAAPI accept(
[in] SOCKET s, // 已开启监听的socket的描述符
[out] sockaddr *addr, // 保存客户端信息的sockaddr结构体的位置,可选
[in, out] int *addrlen // sockaddr结构体的长度,可选
); // 若成功则返回一个已连接的新socket的描述符,
// 否则返回INVALID_SOCKET
// 建立连接 (客户端)
int WSAAPI connect(
[in] SOCKET s, // 已绑定但未连接的socket的描述符
[in] const sockaddr *name, // 包含服务器端信息的sockaddr结构体的位置
[in] int namelen // sockaddr结构体的长度
); // 若成功则返回0,否则返回SOCKET_ERROR
/*
简单示例
*/
/* 服务器端 */
// 声明变量
SOCKET AcceptSocket; // 声明一个接受连接的socket
// 开始监听
// SOMAXCONN会将队列的最大长度置为最大的合理值
listen(sock, SOMAXCONN)
// 同意连接
// NULL表示不保存客户端信息
// 会阻塞进程直到收到客户端发起的连接
AcceptSocket = accept(sock, NULL, NULL);
/* 客户端 */
// 声明变量
struct sockaddr_in clientService; // 声明一个sockaddr_in结构体
// 将服务器端的信息更新到sockaddr_in结构体中
clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr("127.0.0.1");
clientService.sin_port = htons(27015);
// 连接到指定的服务器端socket
// 传参时将sockaddr_in结构体类型转换成sockaddr结构体
connect(sock, (SOCKADDR *) & clientService, sizeof (clientService));
3. 通信
/*
方法
*/
// 发送 (需要连接)
int WSAAPI send(
[in] SOCKET s, // 发送端socket的描述符
[in] const char *buf, // 存有准备发送数据的缓存位置
[in] int len, // 要发送数据的字节数
[in] int flags // 指定调用方式的标志,用按位或运算得出,通常为0
); // 若成功则返回实际发送的字节数,
// 否则返回SOCKET_ERROR
// 接收 (需要连接)
int WSAAPI recv(
[in] SOCKET s, // 接收端socket的描述符
[out] char *buf, // 准备存放接收数据的缓存位置
[in] int len, // 接收缓存的长度
[in] int flags // 指定调用方式的标志,用按位或运算得出,通常为0
); // 若成功则返回实际接收的字节数,
// 否则返回SOCKET_ERROR
// 发送 (不需要连接)
int WSAAPI sendto(
[in] SOCKET s, // 发送端socket的描述符
[in] const char *buf, // 存有准备发送数据的缓存位置
[in] int len, // 要发送数据的字节数
[in] int flags, // 指定调用方式的标志,用按位或运算得出,通常为0
[in] const sockaddr *to, // 保存接收端信息的sockaddr结构体的位置,可选
[in] int tolen // sockaddr结构体的长度,可选
); // 若成功则返回实际发送的字节数,
// 否则返回SOCKET_ERROR
// 接收 (不需要连接)
int WSAAPI recvfrom(
[in] SOCKET s, // 接收端socket的描述符
[out] char *buf, // 准备存放接收数据的缓存位置
[in] int len, // 接收缓存的长度
[in] int flags // 指定调用方式的标志,用按位或运算得出,通常为0
[out] sockaddr *from, // 保存接收端信息的sockaddr结构体的位置,可选
[in, out, optional] int *fromlen // sockaddr结构体的长度,可选
); // 若成功则返回实际接收的字节数,
// 否则返回SOCKET_ERROR
/*
简单示例
*/
/* socket已连接时 */
// 声明变量
char SendBuf[1024]; // 声明一个发送数据缓存
int SendBufLen = 1024; // 发送数据缓存长度
char RecvBuf[1024]; // 声明一个接收数据缓存
int RecvBufLen = 1024; // 接收数据缓存长度
// 发送数据
send(sock, SendBuf, SendBufLen, 0);
// 接收数据
recv(sock, RecvBuf, RecvBufLen, 0);
/* socket无连接时,也可用于已连接的socket,此时最后两个参数为NULL */
// 声明变量
char SendBuf[1024]; // 声明一个发送数据缓存
int SendBufLen = 1024; // 发送数据缓存长度
char RecvBuf[1024]; // 声明一个接收数据缓存
int RecvBufLen = 1024; // 接收数据缓存长度
sockaddr_in RecvAddr; // 声明一个用于保存接收端信息的sockaddr_in结构体
sockaddr_in SendAddr; // 声明一个用于保存发送端信息的sockaddr_in结构体
// 将接收端的信息更新到sockaddr_in结构体中
RecvAddr.sin_family = AF_INET;
RecvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
RecvAddr.sin_port = htons(27015);
// 将服务器端的信息更新到sockaddr_in结构体中
SendAddr.sin_family = AF_INET;
SendAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
SendAddr.sin_port = htons(27015);
// 发送数据
sendto(sock, SendBuf, SendBufLen, 0, (SOCKADDR *) & RecvAddr, sizeof (RecvAddr));
// 接收数据
recvfrom(sock, RecvBuf, RecvBufLen, 0, (SOCKADDR *) & SendAddr, sizeof (SendAddr));
4. 关闭socket
/*
方法
*/
// 关闭指定的socket功能,而不是socket本身,TCP会在传输结束后发送FIN报文段
int WSAAPI shutdown(
[in] SOCKET s, // 准备关闭功能的socket的描述符
[in] int how // 准备关闭的功能的枚举
); // 若成功则返回0,否则返回SOCKET_ERROR
// 关闭socket,TCP会直接发送FIN报文段,可能导致缓存区内数据丢失
int WSAAPI closesocket(
[in] SOCKET s // 准备关闭的socket的描述符
); // 若成功则返回0,否则返回SOCKET_ERROR
/*
简单示例
*/
// 关闭socket的发送功能
shutdown(sock, SD_SEND);
// 关闭socket的接收功能
shutdown(sock, SD_RECEIVE);
// 关闭socket的所有功能
shutdown(sock, SD_BOTH);
// 关闭socket
closesocket(sock);
5. 服务器端完整示例
#ifndef UNICODE
#define UNICODE
#endif
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015
int main() {
//----------------------
// 声明并初始化socket
SOCKET ListenSocket = INVALID_SOCKET;
SOCKET AcceptSocket = INVALID_SOCKET;
// 声明sockaddr_in结构体并更新服务器端信息
sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr("127.0.0.1");
service.sin_port = htons(27015);
// 声明并初始化测试用发送和接收缓存
char *sendbuf = "This is a test from server";
int recvbuflen = DEFAULT_BUFLEN;
char recvbuf[DEFAULT_BUFLEN] = "";
// 声明结果接收变量
int iResult;
//----------------------
// 初始化Windows Socket 2
WSADATA wsaData;
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != NO_ERROR) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
} else
printf("Windows Socket 2 initialized.");
//----------------------
// 创建监听socket
ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
printf("Socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 绑定监听socket
if (bind(ListenSocket, (SOCKADDR *) & service, sizeof (service)) == SOCKET_ERROR) {
printf("Bind failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 开始监听
if (listen(ListenSocket, 1) == SOCKET_ERROR) {
printf("Listen failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
} else
printf("Waiting for client to connect...\n");
// 等待客户端发起连接后建立尝试连接
AcceptSocket = accept(ListenSocket, NULL, NULL);
if (AcceptSocket == INVALID_SOCKET) {
printf("Accept failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
} else
printf("Client connected.\n");
//----------------------
// 发送一个测试数据
iResult = send(AcceptSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("Send failed with error: %d\n", WSAGetLastError());
closesocket(AcceptSocket);
WSACleanup();
return 1;
} else
printf("Bytes sent: %d\n", iResult);
// 发送完成后关闭发送功能
iResult = shutdown(AcceptSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("Shutdown failed with error: %d\n", WSAGetLastError());
closesocket(AcceptSocket);
WSACleanup();
return 1;
}
// 持续接收数据直到对方发送完所有数据
do {
iResult = recv(AcceptSocket, recvbuf, recvbuflen, 0);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed.\n");
else
printf("recv failed with error: %d\n", WSAGetLastError());
} while (iResult > 0);
//----------------------
// 关闭连接
iResult = closesocket(AcceptSocket);
if (iResult == SOCKET_ERROR) {
printf("close failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
WSACleanup();
return 0;
}
5. 客户端完整示例
#ifndef UNICODE
#define UNICODE
#endif
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015
int main() {
//----------------------
// 声明并初始化socket
SOCKET ConnectSocket = INVALID_SOCKET;
// 声明sockaddr_in结构体并更新服务器端信息
sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr("127.0.0.1");
service.sin_port = htons(27015);
// 声明并初始化测试用发送和接收缓存
char *sendbuf = "This is a test from client";
int recvbuflen = DEFAULT_BUFLEN;
char recvbuf[DEFAULT_BUFLEN] = "";
// 声明结果接收变量
int iResult;
//----------------------
// 初始化Windows Socket 2
WSADATA wsaData;
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != NO_ERROR) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
} else
printf("Windows Socket 2 initialized.");
//----------------------
// 创建socket
ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
printf("Socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 连接到服务器端
iResult = connect(ConnectSocket, (SOCKADDR*) &service, sizeof(service));
if (iResult == SOCKET_ERROR) {
printf("Connect failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
} else
printf("Server connected.");
//----------------------
// 发送一个测试数据
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("Send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
} else
printf("Bytes sent: %d\n", iResult);
// 发送完成后关闭发送功能
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("Shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// 持续接收数据直到对方发送完所有数据
do {
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed.\n");
else
printf("recv failed with error: %d\n", WSAGetLastError());
} while (iResult > 0);
//----------------------
// 关闭连接
iResult = closesocket(ConnectSocket);
if (iResult == SOCKET_ERROR) {
printf("close failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
WSACleanup();
return 0;
}
二、Socket的通信流程
在了解了Windows环境下使用的Windows Socket 2中的一些常用API后,还需要简单地介绍一下TCP和UDP使用socket进行通信时的流程。
1. TCP使用socket的通信流程
当使用TCP时,服务器端需要先调用WSAStartup()进行初始化,然后调用socket()和bind()创建一个服务器端的master socket并与端口绑定,再调用listen()来开启监听,最后调用accept()阻塞线程并等待客户端发起连接。而客户端则只需要使用socket()创建一个socket,不需要进行绑定。因为在发送数据时,未绑定的socket会被自动分配一个空闲的端口进行绑定。相反,若客户端选择手动绑定端口,则需要首先确认当前端口是否已被占用,使用起来更加麻烦。客户端在成功创建socket之后,需要调用connect()方法向服务器端发起连接,而服务器端阻塞的accept()在收到客户端发起的连接后新建一个socket与客户端的socket连接并进行通信。此时服务器端新建的socket和客户端的socket都为connected socket,通常服务器端的connected socket会被交给一个新开辟的线程进行管理。此时双方通常会调用send()和recv()来进行数据传递,不过sendto()和recvfrom()同样适用于这种情况,只是方法中的后两个参数会被置为NULL。当任何一方数据发送完成后,会调用shutdown()关闭socket的发送功能。当缓存中的所有数据发送完后,TCP会发送FIN报文段,而对方的recv()则会返回0。此时对方可以选择调用shutdown()关闭socket的接收功能,或是在已经关闭发送功能时选择直接调用closesocket()关闭socket。
2. UDP使用socket的通信流程
当使用UDP时,服务器端在调用WSAStartup()进行初始化后,只需要使用socket()和bind()创建一个服务器端的socket并与端口绑定,就可以开始等待接收数据。客户端则和使用TCP时一样,只需要使用socket()进行创建,而不需要进行绑定。在双方socket都创建完成后,客户端可以选择通过sendto()和recvfrom()来直接进行数据的交互,也可以选择先调用connect()后再使用send()和recv()来进行数据的传递。但是与TCP中使用connect()时不同,UDP本身并不需要建立连接,也不会像TCP一样进行三次握手,因此调用connect()并不会与对方建立实际的连接,而只是创建了默认的目的地地址信息供send()和recv()使用。或者换句话说,调用connect()后,客户端的socket与服务器端的socket建立了传输层以上的、形式上的连接。而使用以上两种方式进行数据交互的不同在于,使用sendto()和recvfrom()来进行交互的底层是,每次的数据传输都需要经过建立连接、发送数据和关闭连接三个步骤;而在调用connect()后再使用send()和recv()来进行交互就不需要多次的建立和关闭连接,而是可以连续发送数据。当客户端完成数据的发送后,同样会调用shutdown()关闭socket的发送功能,再调用closesocket()关闭socket。