一.概述:
本次练习的是TCP/UDP套接字编程,使用的是winsocket,对主要的库函数进行简绍,并实现了一个程序:实现服务器与客户端之间的通信,在服务器端实现记录用户名和密码,客服端可以实现用户名和密码的输入和查找,并且检查是否匹配。(参考 <<Visual C++网络编程>>)
PS: 127.0.0.1是回路地址,用于在同一台机器上测试代码。端口号要大于1024。
二.基于TCP/UDP协议的套接字编程详解:
基于 TCP 的套接字编程的所有客户端和服务器端都是从调用socket 开始,它返回一个套接字描述符。客户端随后调用connect 函数,服务器端则调用 bind、listen 和accept 函数。套接字通常使用标准的close 函数关闭,但是也可以使用 shutdown 函数关闭套接字。下面针对套接字编程实现过程中所调用的函数进程分析。以下是基于 TCP 套接字编程的流程图:
典型的UDP客户/服务器程序函数调用图:
三.相关数据结构:
(1).sockaddr:sockaddr用来保存一个套接字
struct sockaddr { unsigned short int sa_family; //指定通信地址类型,如果是TCP/IP通信,则值为AF_inet char sa_data[14]; //最多用14个字符长度,用来保存IP地址和端口信息}; }
(2).
sockaddr_in的功能与socdaddr相同,也是用来保存一个套接字的信息,不同的是将IP地址与端口分开为不同的成员,定义如下:
struct sockaddr_in { unsigned short int sin_family; //指定通信地址类型 uint16_t sin_port; //套接字使用的端口号 struct in_addr sin_addr; //需要访问的IP地址 unsigned char sin_zero[8]; //未使用的字段,填充为0}; }
在这一结构中,in_addr也是一个结构体,定义如下,用于保存一个IP地址:
struct in_addr { uint32_t s_addr; };
(3).WSAData:包含Winsock库的版本信息,这个结构是在调用函数WSAStartup时由系统填入。
struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYSSTATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
};
wVersion为你将使用的Winsock版本号,
wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。
szDescription与szSystemStatus由特定版本的Winsock设置,实际上没有太大用处。
iMaxSockets表示最大数量的并发Sockets,其值依赖于可使用的硬件资源。
iMaxUdpDg表示数据报的最大长度;然而,获取数据报的最大长度,你需要使用WSAEnumProtocols对协议进行查询。
(4).SOCKET:即套接字句柄,为一个32位的整数。
typedef unsigned int SOCKET
四.Winsock相关函数:
(1).WSAStartup函数:初始化Winsock
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData)
wVersionRequested参数:是一个WORD(双字节)数值,它指定了应用程序需要使用的Winsock版本.
主版本号在 低字节, 次版本号在 高字节。
实例:希望版本号为 1.2 可以如下代码:
wVersionRequested = 0x0201。
lpWSAData参数:指向WSADATA数据结构的指针,该结构用于返回本机的Winsock系统实现的信息.
该结构WhighVersion和wVersion两个域系统支持的最高版本,后者是系统希望调用者使用的版本.
函数成功 返回0; 否则返回错误码.
一般可以这样初始化:
WSADATA wsaData;
WSAStartup(0x0202, &wsaData);
(2).WSACleanup函数:Winsock程序在退出之前都必须要调用WSAClernup,以便系统可以释放资源。
int WSACleanup(void)
(3).WSAGetLastError函数:当一个Winsock函数返回一个失败值时,调用这个函数可以获取具体的失败原因。
int WSAGetLastError(void)
(4).inet_addr函数:地址转换函数
ussigned long inet_addr(const char FAR* cp)
例如:inet_addr("127.0.0.1")
(5).字节序转换函数:
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntonl(u_long netlong);
u_short ntons(u_short netshort);
htonl和htons用于把主机号字节序转换为网络字节序,ntonl和ntons则相反。(host:主机)
五.与实现通信相关的函数:
(1).sock函数:返回一个 套接字句柄。
int socket(int family, int type, int protocol);
- * 说明:
- * socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;
- * family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:
- * (1)AF_INET IPv4因特网域
- * (2)AF_INET6 IPv6因特网域
- * (3)AF_UNIX Unix域
- * (4)AF_ROUTE 路由套接字
- * (5)AF_KEY 密钥套接字
- * (6)AF_UNSPEC 未指定
- *
- * type确定socket的类型,常用类型如下:
- * (1)SOCK_STREAM 有序、可靠、双向的面向连接字节流套接字
- * (2)SOCK_DGRAM 长度固定的、无连接的不可靠数据报套接字
- * (3)SOCK_RAW 原始套接字
- * (4)SOCK_SEQPACKET 长度固定、有序、可靠的面向连接的有序分组套接字
- *
- * protocol指定协议,常用取值如下:
- * (1)0 选择type类型对应的默认协议
- * (2)IPPROTO_TCP TCP传输协议
- * (3)IPPROTO_UDP UDP传输协议
- * (4)IPPROTO_SCTP SCTP传输协议
- * (5)IPPROTO_TIPC TIPC传输协议
(2).closesocket函数:关闭一个套接字。
int closesocket(SOCKET s);
传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。
(3).shutdown函数:停止 Socket 接收/传送的功能
int shutdown(SOCKET s,int how)
参数: s :Socket 的识别码,
how :代表该停止那些动作的标帜
传回值: 成功返回 0 ,失败 返回 SOCKET_ERROR 。
若 how 的值为 0,则不再接收资料。
若 how 的值为 1,则不再允许传送资料。
若 how 的值为 2,则不再接收且不再传送资料。
注意:shutdown() 函式并没有将 Socket 关闭,所以该 Socket 所占用之资源必须在呼叫closesocket() 之后才会释放。
(4).bind函数:把相关套接字句柄绑定到addr地址,绑定之后客户端/服务器就可以通过该地址连接到服务器/客户端。
int bind(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
s参数: 为一个套接字句柄;
addr参数:是一个指向特定协议地址结构的指针;
addrlen参数:是地址结构的长度;
传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。
PS:当addr结构体中的sin_addr.s_addr为INADDR_ANY时,表示绑定到通配地址(即没有指定的mac地址),这时可以接受来自所有网络接口的连接请求,适合于有多个网卡的情况。
(5).listen函数:设定 Socket 为监听状态,准备被连接。
int listen(SOCKET s,int backlog)
参 数: s Socket 的识别码,
backlog 未真正完成连接前彼端的连接要求的最大个数 (连接队列中的最大数)
传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。
(6).accept函数:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠。
int accept(SOCKER s, struct sockaddr FAR* addr, int FAR* addrlen)
参数 s:已经绑定并进入监听状态的套接字句柄。
addr:用于保存客户端的地址信息。
addrlen:用于保存客户端的地址信息的结构体大小
PS:若没有连接请求等待处理,accept会阻塞直到一个请求到来;
返回值:若成功返回套接字描述符,出错返回INVALID_SOCKET;如果成功,此时的套接字为已连接套接字,后面的通信是用这个已连接套接字来描述的。
(7).recv函数:用于TCP流式套接字编程中接受来自客户端的消息。
int recv(SOCKET s, char FAR* buf, int len, int flags)
参数:s:accept返回的一个已连接套接字
buf:用于接受数据的缓存区
len:buf的长度,以字节问单位。
flags:用来影响recv的行为,一般为0;
返回值:成功时,返回实际接收的字节数,失败返回SOCKET_ERROR。
PS:recv为阻塞式的等待有数据可接收。
(8).send函数:用于TCP流式套接字编程中发送消息到客户端。
int send(SOCKET s, char FAR* buf, int len, int flags)
参数:s:accept返回的一个已连接套接字
buf:用于发送数据的缓存区
len:buf的长度,以字节问单位。
flags:用来影响recv的行为,一般为0;
返回值:成功时,返回实际发送的字节数,失败返回SOCKET_ERROR。
(9).connect函数:连接服务器地址,使s成为一个已连接套接字。
int connect(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
s参数: 为一个套接字句柄;
addr参数:是一个指向特定协议地址结构的指针;
addrlen参数:是地址结构的长度;
传回值: 成功 返回0 ,此时代表已经和服务器连接成功,s成为一个已连接套接字; 失败 返回 SOCKET_ERROR 。
(10).recvfrom函数:用于UDP报文套接字编程中接受来自地址为from的主机消息。
int recvfrom(SOCKET s, char FAR* buf, int len, int flags,
struct socket FAR* from, int FAR* fromlen)
参数:s为一个已连接套接字。
buf:数据接收缓冲区。
len:数据接收缓存区的大小。
flags:用于控制recvfrom行为的一些标志,一般为0.
from:对方的套接字地址。
fromlen:对方的套接字地址结构的大小。
返回值:成功返回实际接受的字节数,失败返回SOCKET_ERROR;如果buf大小不足以容纳接收到的数据报,那么返回一个WSAEMSGSIZE错误。
PS:UDP套接字编程中的接收和发送信息都是以数据报为单位的。每次recvfrom都是接收一个独立的数据报,不同的recvfrom所接收的数据报之间没有任何关系(即不会出现一个recvfrom接收一个数据报的一部分,另一个接收这个数据报的另一部分的情况)。
(11)..sendto函数:用于UDP报文套接字编程中发送到地址为from的主机消息。
int sendto(SOCKET s, char FAR* buf, int len, int flags,
struct socket FAR* to, int FAR* tolen)
参数:s为一个已连接套接字。
buf:发送数据的缓冲区。
len:发送数据的缓存区大小。
flags:用于控制sendto行为的一些标志,一般为0.
to:对方的套接字地址。
tolen:对方的套接字地址结构的大小。
返回值:成功返回实际发送的字节数,失败返回SOCKET_ERROR;要发送的字节数为len时,如果len大于UDP数据报的最大值,那么返回一个WSAEMSGSIZE错误。
PS:对于UDP而言,sendto的行为是全部或者没有(all or nothing),要么成功发送所有要求发送的字节,要么发送失败。
六.实际步骤参考上图。
七.实现功能的代码:
(1)TCP套接字编程:
服务器:
#include<iostream> #include<string.h> #include<winsock2.h> #pragma comment(lib,"Ws2_32.lib") using namespace std; struct Data { char* _usser; char* _possword; Data() :_usser(new char[1024]) , _possword(new char[1024]) {} }; const int PORT = 2000; const int LEN = 1024; char buf[LEN]; Data messeage[LEN]; SOCKET sListen; sockaddr_in saListen; sockaddr_in saClient; SOCKET serverToClient; void Find() //查找用户名 和 密码是否匹配存在 { char* msUsser = "请输入你要查找的用户名:"; send(serverToClient, msUsser, strlen(msUsser), 0); memset(buf, 0, LEN); int ret = recv(serverToClient, buf, LEN, 0);//接收用户名 for (size_t i = 0; i < LEN; i++) { if (strcmp(messeage[i]._usser,buf) == 0) { char* msPsw = "请输入所查找用户名的密码:"; send(serverToClient, msPsw, strlen(msPsw), 0); memset(buf, 0, LEN); int ret = recv(serverToClient, buf, LEN, 0);//接收密码 if (strcmp(messeage[i]._possword, buf) == 0) //s注意用trcmp函数 { char* ms = "用户名和密码匹配存在\n请选择->"; send(serverToClient, ms, strlen(ms), 0); } else { char* mssage = "用户名正确 密码错误\n请选择->"; send(serverToClient, mssage, strlen(mssage), 0); } memset(buf, 0, LEN); recv(serverToClient, buf, LEN, 0); return; } } char* msUsserNot = "用户名不存在\n请选择->"; send(serverToClient, msUsserNot, strlen(msUsserNot), 0); memset(buf, 0, LEN); recv(serverToClient, buf, LEN, 0); } void Record() { size_t index = 0; while (true) { while(buf[0] == ‘2‘) //只有用户名和密码都输入完才能查找// while { Find(); } if (buf[0] == ‘0‘) //如果客服端输入0 则退出连接 { char* ms = "退出成功"; int len = strlen(ms); send(serverToClient, ms, len, 0); cout << "服务器退出连接!!!" << endl; return; } char* msUsser = "请输入用户名:"; int len = strlen(msUsser); send(serverToClient, msUsser, len, 0); memset(buf, 0, LEN); int ret = recv(serverToClient, buf, LEN, 0);//接收用户名 if (ret == SOCKET_ERROR) //表示接收失败 { cout << "Client closed !!!" << endl; int size = sizeof(sockaddr); serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 cout << "accept new" << endl; } strcpy_s(messeage[index]._usser,1024, buf); //注意 用户名不用++ char* msPsw = "请输入密码:"; send(serverToClient, msPsw, strlen(msPsw), 0); memset(buf, 0, LEN); int retTwo = recv(serverToClient, buf, LEN, 0);//接收密码 if (retTwo == SOCKET_ERROR) //表示接收失败 { cout << "Client closed !!!" << endl; int size = sizeof(sockaddr); serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 cout << "accept new" << endl; } strcpy_s(messeage[index++]._possword,1024, buf); memset(buf, 0, LEN); char* choice = "请选择->"; send(serverToClient, choice, strlen(choice), 0); memset(buf, 0, LEN); recv(serverToClient, buf, LEN, 0);//接收客户的选择 } } int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); //初始化winsock sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个套接字 //绑定分配的 port 和 ip地址 saListen.sin_family = AF_INET; saListen.sin_addr.s_addr = htonl(INADDR_ANY); saListen.sin_port = htons(PORT); bind(sListen, (sockaddr*)&saListen, sizeof(sockaddr)); listen(sListen, 5); //进入监听状态 int size = sizeof(sockaddr); serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //创建一个连接套接字 int ret = recv(serverToClient, buf, LEN, 0); if (ret == SOCKET_ERROR) //表示接收失败 { cout << "Client closed !!!" << endl; int size = sizeof(sockaddr); serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 cout << "accept new" << endl; } Record(); return 0; }
客户端:
//本次练习存在很多漏洞 //当相同的代码段重复出现多次时 变量的命名很重要 //客户端只要接受和发送信息就可 关键协议在服务端实现 //注意 send 与 recv 要匹配使用 #include<iostream> #include<winsock2.h> #pragma comment(lib,"WS2_32.lib") using namespace std; const int PORT = 2000; const int LEN = 1024; int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); SOCKET client; client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in saServer = {0}; saServer.sin_family = AF_INET; saServer.sin_addr.s_addr = inet_addr("127.0.0.1"); saServer.sin_port = htons(PORT); int ret = connect(client, (sockaddr*)&saServer, sizeof(saServer)); if (ret == 0) { printf("connect access :%d:%d\n", saServer.sin_addr.s_addr, saServer.sin_port); } else { printf("connect faild :%d:%d\n", saServer.sin_addr.s_addr, saServer.sin_port); } cout << "............0:退出 1:输入 2:查找 ..............." << endl; char buf[LEN]; printf("请选择-> "); gets_s(buf); send(client, buf, strlen(buf), 0); while (true) { memset(buf, 0, LEN); int retUsser = recv(client, buf, LEN, 0); buf[retUsser] = ‘\0‘; cout << buf << endl; gets_s(buf); send(client, buf, strlen(buf), 0); } return 0; }
执行结果:
(2).UDP套接字编程:
服务器:
#include<iostream> #include<string.h> #include<winsock2.h> #pragma comment(lib,"Ws2_32.lib") using namespace std; struct Data { char* _usser; char* _possword; Data() :_usser(new char[1024]) , _possword(new char[1024]) {} }; const int PORT = 2000; const int LEN = 1024; char buf[LEN]; Data messeage[LEN]; SOCKET sListen; //服务器套接字 sockaddr_in saListen; //本地套接字地址 sockaddr_in saClient; //客户端套接字地址 //SOCKET serverToClient; //已连接套接字 int clientSize = sizeof(saClient); // ....改.... void Find() //查找用户名 和 密码是否匹配存在 { char* msUsser = "请输入你要查找的用户名:"; sendto(sListen, msUsser, strlen(msUsser), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); int ret = recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize);//接收用户名 // ....改.... for (size_t i = 0; i < LEN; i++) { if (strcmp(messeage[i]._usser, buf) == 0) { char* msPsw = "请输入所查找用户名的密码:"; sendto(sListen, msPsw, strlen(msPsw), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); int ret = recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize);//接收密码 // ....改.... if (strcmp(messeage[i]._possword, buf) == 0) //s注意用trcmp函数 { char* ms = "用户名和密码匹配存在\n请选择->"; sendto(sListen, ms, strlen(ms), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... } else { char* mssage = "用户名正确 密码错误\n请选择->"; sendto(sListen, mssage, strlen(mssage), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... } memset(buf, 0, LEN); recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize); // ....改.... return; } } char* msUsserNot = "用户名不存在\n请选择->"; sendto(sListen, msUsserNot, strlen(msUsserNot), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize); // ....改.... } void Record() { size_t index = 0; while (true) { while (buf[0] == ‘2‘) //只有用户名和密码都输入完才能查找// while { Find(); } if (buf[0] == ‘0‘) //如果客服端输入0 则退出连接 { char* ms = "退出成功"; int len = strlen(ms); sendto(sListen, ms, len, 0, (SOCKADDR*)&saClient, clientSize); // ....改.... cout << "服务器退出连接!!!" << endl; return; } char* msUsser = "请输入用户名:"; int messageLen = strlen(msUsser); sendto(sListen, msUsser, messageLen, 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); int ret = recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize);//接收用户名 // ....改.... if (ret == SOCKET_ERROR) //表示接收失败 { cout << "recvfrom error !!!" << endl; // int size = sizeof(sockaddr); // ....改.... // serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 // ....改.... // cout << "accept new" << endl; // ....改.... } strcpy_s(messeage[index]._usser, 1024, buf); //注意 用户名不用++ char* msPsw = "请输入密码:"; sendto(sListen, msPsw, strlen(msPsw), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); int retTwo = recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize);//接收密码 // ....改.... if (retTwo == SOCKET_ERROR) //表示接收失败 { cout << "recvfrom error !!! !!!" << endl; // int size = sizeof(sockaddr); // ....改.... // serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 // ....改.... // cout << "accept new" << endl; // ....改.... } strcpy_s(messeage[index++]._possword, 1024, buf); memset(buf, 0, LEN); char* choice = "请选择->"; sendto(sListen, choice, strlen(choice), 0, (SOCKADDR*)&saClient, clientSize); // ....改.... memset(buf, 0, LEN); recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize);//接收客户的选择 // ....改.... } } int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); //初始化winsock sListen = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //创建一个套接字 ....改.... //绑定分配的 port 和 ip地址 saListen.sin_family = AF_INET; saListen.sin_addr.s_addr = htonl(INADDR_ANY); saListen.sin_port = htons(PORT); bind(sListen, (sockaddr*)&saListen, sizeof(sockaddr)); // listen(sListen, 5); //进入监听状态 // int size = sizeof(sockaddr); // serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //创建一个连接套接字 int ret = recvfrom(sListen, buf, LEN, 0, (SOCKADDR*)&saClient, &clientSize); // ....改.... if (ret == SOCKET_ERROR) //表示接收失败 { cout << "recvfrom error !!!" << endl; // int size = sizeof(sockaddr); // ....改.... // serverToClient = accept(sListen, (sockaddr*)&saClient, &size); //重新创建一个连接套接字 // ....改.... // cout << "accept new" << endl; // ....改.... } Record(); return 0; }
客户端:
//本次练习存在很多漏洞 //当相同的代码段重复出现多次时 变量的命名很重要 //客户端只要接受和发送信息就可 关键协议在服务端实现 //注意 send 与 recv 要匹配使用 #include<iostream> #include<winsock2.h> #pragma comment(lib,"WS2_32.lib") using namespace std; const int PORT = 2000; const int LEN = 1024; int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); SOCKET client; client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // ....改.... sockaddr_in saServer = { 0 }; saServer.sin_family = AF_INET; saServer.sin_addr.s_addr = inet_addr("127.0.0.1"); saServer.sin_port = htons(PORT); int ret = connect(client, (sockaddr*)&saServer, sizeof(saServer)); if (ret == 0) { printf("connect access :%d:%d\n", saServer.sin_addr.s_addr, saServer.sin_port); } else { printf("connect faild :%d:%d\n", saServer.sin_addr.s_addr, saServer.sin_port); } cout << "............0:退出 1:输入 2:查找 ..............." << endl; char buf[LEN]; int serverSize = sizeof(saServer); // ....改.... printf("请选择-> "); gets_s(buf); int retlen = sendto(client, buf, strlen(buf), 0, (SOCKADDR*)&saServer, sizeof(saServer)); // ....改.... if (retlen == WSAEMSGSIZE) // ....改.... { cout << "over size !" << endl; } else if (retlen == 0) { cout << "sendto error !" << endl; } while (true) { memset(buf, 0, LEN); int retUsser = recvfrom(client, buf, LEN, 0, (SOCKADDR*)&saServer, &serverSize); // ....改.... if (retlen == WSAEMSGSIZE) { cout << "over size !" << endl; } else if (retlen == SOCKET_ERROR) { cout << "sendto error !" << endl; } buf[retUsser] = ‘\0‘; cout << buf << endl; gets_s(buf); sendto(client, buf, strlen(buf), 0, (SOCKADDR*)&saServer, sizeof(saServer)); // ....改.... } return 0; }
执行结果:
总结:TCP是面向连接的协议,所以TCP的套接字编程要bind本地套接字地址,进入listen状态,accept得到一个已连接套接字,之后才能接收和发送消息,而UDP是面向无连接的,所以只要bind本地套接字地址之后就可以发送和接收消息了。
服务器要先接收消息再发送信息,而客户端是先发送信息再接收消息。
客服端一般可以省略绑定本地套接字地址,因为系统会自动为客服端分配一个本地IP地址和本地端口。