Socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。
将socket通信类比为打电话这一生活场景。这里我把TCP服务器比作政府某一服务部门能,TCP客户端比作企业中某一部门电话,描述这一过程,恰好就像是socket通信,服务部门提供服务,企业部门申请服务。
要实现通信,首先政府部门都必须申请一个电话(socket_fd),并向有关部门注册(我们的系统),提供地址(sockadrr)以及属于哪个部门的(port),录入系统后,就算是合约生效了(bind),于是乎,政府广而告之,这个服务热线就算开通了,在部门里面的人员所需要做的事情,就是等待企业家拨打热线(listen)。
企业家拨打电话对地点和部门没有这么多的要求了,他并不需要绑定地址和部门,在任何一个可以拨打电话的地方(可能是同个部门,也可以同公司不同部门,甚至可能是竞争对手),他只需要拿起一个已经注册的电话(socket_fd),拨打电话(connect)
政府部门接通电话(accept)后,桥梁就打通了(服务者client_fd、顾客server_fd),可以进行听说了(read write)。企业家咨询完成(close),政府到点下班关闭服务(close)
什么是TCP/IP、UDP
`TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
这里有一张图,表明了这些协议的关系。`
* 1
* 2
* 3
socket中TCP的三次握手建立连接
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回,这也是dos攻击的基本原理。
socket中TCP的四次握手释放连接
上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
socket通信流程
socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程基本如下图所示:
socket的基本操作
既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。
socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
除了socket()函数之外还有其它bind(),accept()等函数,这些函数在下面的例子中依次介绍:
服务器端代码
`#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
using namespace std;
char Ip[20][200] = { '0' };
int iConnect = 0; //当前客户端数量
/*
DWORD是无符号的,相当于unsigned long ,它是MFC的数据类型。DWORD一般用于返回值不会出现负值情况
WINAPI是一个宏,所代表的符号是__stdcall, 函数名前加上这个符号表示这个函数的调用约定是标准调用约定,windows API函数采用这种调用约定
LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。 可以将其理解为long型的指针,指向void型
*/
DWORD WINAPI threadpro(LPVOID pParam) //创建多线程函数,函数返回值为DWORD WINAPI
{
SOCKET hsock = (SOCKET)pParam; //把pParam转换为SOCKET型指针
char buffer[1024];
char sendBuffer[1024];
if (hsock != INVALID_SOCKET) INVALID_SOCKET表示无效
cout<<"Start receive information from IP:"<< Ip[iConnect] << endl << endl;
while (true) //循环接收发送的内容
{
int num = recv(hsock, buffer, 1024, 0); //阻塞函数,等待接受内容
if (num <= 0)
{
cout <<"Client with IP:"<= 0)
cout << "Information from:" << Ip[iConnect] << ":" << buffer << endl << endl;
if (!strcmp(buffer, "AAA")) //如果接受到 AAA 返回 BBB
{
memset(sendBuffer, 0, 1024);
strcpy(sendBuffer, "BBB");
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0); //回送信息
cout << "The message sent to IP:"< iMaxConnect) //判断连接数是否大于最大连接数
{
int ires = send(m_Server[iConnect], WarnBuf, sizeof(WarnBuf), 0);
}
else
{
HANDLE m_Handle; //线程句柄
DWORD nThreadId = 0; //线程ID
nThreadId++;
/*
当使用CreateProcess调用时,系统将创建一个进程和一个主线程。CreateThread将在主线程的基础上创建一个新线程,大致做如下步骤:
1在内核对象中分配一个线程标识/句柄,可供管理,由CreateThread返回
2把线程退出码置为STILL_ACTIVE,把线程挂起计数置1
3分配context结构
4分配两页的物理存储以准备栈,保护页设置为PAGE_READWRITE,第2页设为PAGE_GUARD
5lpStartAddr和lpvThread值被放在栈顶,使它们成为传送给StartOfThread的参数
6把context结构的栈指针指向栈顶(第5步)指令指针指向startOfThread函数
GreateThread语法如下:
hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ;
第一个参数是指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,它被设为NULL。
第二个参数是用于新线程的初始堆栈大小,默认值为0。在任何情况下,Windows根据需要动态延长堆栈的大小。
第三个参数是指向线程函数的指针。函数名称没有限制,但是必须以下列形式声明:
DWORD WINAPI ThreadPro (PVOID pParam) ;
CreateThread的第四个参数为传递给ThreadProc的参数。这样主线程和从属线程就可以共享数据。
CreateThread的第五个参数通常为0,但当建立的线程不马上执行时为旗标CREATE_SUSPENDED。线程将暂停直到呼叫ResumeThread来恢复线程的执行为止。
第六个参数是一个指标,指向接受执行绪ID值的变量。
*/
m_Handle = (HANDLE)::CreateThread(NULL, 0, threadpro, (LPVOID)m_Server[--iConnect], 0, &nThreadId); //启动线程
}
}
}
/*
WSACleanum语法如下:
int WSACleanup(void);
*/
WSACleanup(); //用于释放ws2_32.dll动态链接库初始化时分配的资源
}`
* 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
* 81
* 82
* 83
* 84
* 85
* 86
* 87
* 88
* 89
* 90
* 91
* 92
* 93
* 94
* 95
* 96
* 97
* 98
* 99
* 100
* 101
* 102
* 103
* 104
* 105
* 106
* 107
* 108
* 109
* 110
* 111
* 112
* 113
* 114
* 115
* 116
* 117
* 118
* 119
* 120
* 121
* 122
* 123
* 124
* 125
* 126
* 127
* 128
* 129
* 130
* 131
* 132
* 133
* 134
* 135
* 136
* 137
* 138
* 139
* 140
* 141
* 142
* 143
* 144
* 145
* 146
* 147
* 148
* 149
* 150
* 151
* 152
* 153
* 154
* 155
* 156
* 157
* 158
* 159
* 160
* 161
* 162
* 163
* 164
* 165
* 166
* 167
* 168
* 169
* 170
* 171
* 172
* 173
* 174
* 175
* 176
* 177
* 178
* 179
* 180
* 181
* 182
* 183
* 184
* 185
* 186
* 187
* 188
* 189
* 190
* 191
* 192
* 193
* 194
* 195
* 196
* 197
* 198
* 199
* 200
* 201
* 202
* 203
* 204
* 205
* 206
* 207
* 208
* 209
* 210
* 211
* 212
* 213
* 214
* 215
* 216
* 217
* 218
* 219
* 220
* 221
* 222
* 223
* 224
* 225
* 226
* 227
* 228
* 229
* 230
* 231
* 232
* 233
* 234
* 235
* 236
* 237
* 238
* 239
* 240
* 241
* 242
* 243
* 244
* 245
* 246
* 247
* 248
* 249
* 250
* 251
* 252
* 253
* 254
* 255
* 256
* 257
* 258
* 259
* 260
* 261
* 262
* 263
* 264
* 265
* 266
* 267
* 268
* 269
* 270
* 271
* 272
* 273
* 274
* 275
* 276
* 277
* 278
* 279
* 280
* 281
* 282
* 283
* 284
* 285
* 286
* 287
* 288
* 289
* 290
* 291
* 292
* 293
* 294
* 295
* 296
* 297
* 298
* 299
* 300
客户端代码
`#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
using namespace std;
int main(void)
{
WSADATA wsd; //定义WSADATA对象
/*
WSAStartup错误码介绍
WSASYSNOTREADY 网络通信中下层的网络子系统没准备好
WSAVERNOTSUPPORTED Socket实现提供版本和socket需要的版本不符
WSAEINPROGRESS 一个阻塞的Socket操作正在进行
WSAEPROCLIM Socket的实现超过Socket支持的任务数限制
WSAEFAULT lpWSAData参数不是一个合法的指针
*/
/*
当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0。
例:假如一个程序要使用2.2版本的Socket,那么程序代码如下
*/
//WSAStartup(MAKEWORD(2, 2), &wsd); //初始化套接字
WSAStartup(MAKEWORD(2, 2), &wsd);
SOCKET m_SockClient; //创建socket对象
/*
sockaddr_in的定义如下
struct sockaddr_in
{
short int sin_family; // Address family 一般来说 AF_INET(地址族)PF_INET(协议族 )
unsigned short int sin_port; //sin_port存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留
struct in_addr sin_addr; //存储IP地址
unsigned char sin_zero[8]; //sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};
*/
sockaddr_in clientaddr; //服务器信息
clientaddr.sin_family = AF_INET;
clientaddr.sin_port = htons(4600);
clientaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
/*
socket的定义如下
SOCKET WSAAPI socket(
_In_ int af,
_In_ int type,
_In_ int protocol
);
建立一个socket用于连接
af:address family,如AF_INET
type:连接类型,通常是SOCK_STREAM或SOCK_DGRAM
protocol:协议类型,通常是IPPROTO_TCP或IPPROTO_UDP
返回值:socket的编号,为-1表示失败
*/
m_SockClient = socket(AF_INET, SOCK_STREAM, 0);
/*
WINSOCK_API_LINKAGE
int
WSAAPI
connect(
SOCKET s,
const struct sockaddr FAR * name,
int namelen
);
第一个参数是客户端的套接字(表明即将发起连接请求),第二个参数是服务端的套接字所在的“地方”(“地方”是我自定义的专有名词),
第三个参数是该“地方”的大小
如果请求连接成功,则返回0,否则返回错误码。
*/
int i = connect(m_SockClient, (sockaddr*)&clientaddr, sizeof(clientaddr));
cout << "Connection status " << i << endl;
char buffer[1024];
char inBuf[1024];
int num;
num = recv(m_SockClient, buffer, 1024, 0); //阻塞函数,等待接受内容
if (num > 0) //阻塞
{
cout << "Receive form server" << buffer << endl;
while (true)
{
num = 0;
cin >> inBuf;
if (!strcmp(inBuf, "exit")) //如果输入的是exit则断开连接
{
send(m_SockClient, inBuf, sizeof(inBuf), 0);
return 0;
}
send(m_SockClient, inBuf, sizeof(inBuf), 0);
num = recv(m_SockClient, buffer, 1024, 0);
if (num >= 0)
cout << "Receive form server: " << buffer << endl; //输出接受到的内容
}
}
}`
* 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
* 81
* 82
* 83
* 84
* 85
* 86
* 87
* 88
* 89
* 90
* 91
* 92
* 93
* 94
* 95
* 96
* 97
* 98
* 99
* 100
* 101
* 102
* 103
* 104