无连接,不可靠的数据报协议 --> 简单,快捷
域名系统,简单网络管理协议(SNMP), 网络文件系统(NFS), 动态主机配置协议( DHCP), 实时传输协议RTP
服务器端:
- 创建socket
- bind到本地地址
- recvfrom, 接收数据并保存发送方的地址
- sendto 发送数据
没有连接建立,维护,终止所带来的开销
使用UDP的应用程序要自己负责数据的重传以及确认,解决可靠性相关的问题
socket
建立的socket用于之后的函数调用中标识套接口,为套接口分配了要使用的资源
SOCKET WSAAPI socket(int af,int type, int protocol);
- return
成功返回类型为SOCKET的描述符,是已经被创建的新的套接口
失败返回INVALID_SOCKET, 可以调用WSAGetLastError() 得到具体的错误码 - af地址族
早期设想使用PF_*表示协议簇,而用AF_*表示地址簇, 实际上,直到现在支持多个地址簇的协议簇还没有出现
在WinSock实现中,把所有的PF_*都定义成了AF_*对应的值
常用的地址簇有AF_INET、AF_INET6、AF_LOCAL
套接字跨平台: 协议平台
- type 协议类型
功能的三要素: 服务 协议 功能 - protocol 套接口使用的特定协议
参数protocol与type是相关, type规定的是大的类别,而protocol是这一类中具体的协议
当套接口类型只支持一个协议时,protocol可以设置为0,如SOCK_STREAM、SOCK_DGRAM
bind
bind函数给套接口指定一个本地地址( IP 地址 + Port)
函数bind既可以用于面向连接的socket,也可以用于无连接socket,它要在connect或listen前调用。
int WSAAPI bind(SOCKET s,const struct sockaddr FAR * name, int namelen);
- return
成功返回0, 失败返回SOCKET_ERROR, 可以调用WSAGetLastError得到具体的错误码 - name指向sockaddr结构, 是一个通用的地址结构
- 成员sa_family指明协议对应的地址簇 --> 不同的协议簇可以使用不同的地址格式
编程时使用与协议相关的地址结构,但在传递给socket接口函数时,需要把它转换成通用socket地址结构
- Internet协议地址包括三部分:地址簇、主机地址和端口号 --> 地址簇sa_family为常量AF_INET
- 不关心本地地址时,它可以把地址设置为常量INADDR_ANY
- 如果没有指定端口,即端口值为0,TCP/IP协议将为应用程序分配一个唯一端口号
- 如果应用程序想知道系统为它分配的地址和端口,可以在调用bind后使用getsockname函数
- 在多宿主机上, 地址设置为INADDR_ANY,除非socket已经连接,调用了connect函数或者UDP发送了数据,系统才为socket选择一个本地地址,否则getsockname不一定能够得到本地地址,因为主机上有多个地址都是有效的。
- 如果不调用bind,当调用connect或listen函数时,系统会为socket选择本地地址和临时端口 --> 客户端常见
- IP地址: 服务器一般不明确指定,而是由系统帮助选择,根据目的地址为数据报选择一条路由
- 客户端程序通常不关心本地地址和端口,可以不调用bind函数,本地地址和端口全由系统选择
sendto
sendto把数据发送到特定的目的地址 --> 常用于无连接socket(UDP)
int WSAAPI sendto(SOCKET s, const char FAR* buf,int len, int flags,const struct sockaddr FAR*to,int tolen);
- return
成功返回发送数据的字节长度(可能小于参数len要求发送的长度)
失败返回SOCKET_ERROR, 可以调用 WSAGetLastError() 得到具体的错误码 - s 套接口描述符
- buf 要发送的数据
- len 数据的长度
- flags 规定了调用的方式
- to 发送的目的地址
参数to规定数据报要发送到的目的地址
参数to可以是任何有效的地址,包括广播或者多播
为了向广播地址发送数据,应用程序必须调用setsockopt(SO_BROADCAST)开启广播功能,广播地址由宏INADDR_BROADCAST定义,to中的IP地址要设置为该值,发送到广播地址的数据报不建议分片,数据的部分最好不要超过512字节
- tolen 地址to 的长度
说明:
- 调用sendto时,如果socket还没有绑定,系统将为socket分配一个本地地址,并把socket标识为已经绑定,应用程序可以调用getsockname得到绑定的本地地址
- 对于数据报socket,一次可以发送的数据量受底层网络最大分组大小的限制,可以调用getsockopt(SO_MAX_MSG_SIZE)得到
如果程序发送的数据长度超过了底层协议的限制,则sendto返回WSAEMSGSIZE,并且不会发送数据。 - sendto成功并不表示数据已经发送到网络上,只意味着系统已经接受了应用程序的数据,并保存到socket的缓冲区队列中,数据什么时候发送出去,由系统决定,应用程序并不知道。
- 如果传输协议没有空间保存应用程序的数据,除非用户已经把socket设置为非阻塞模式,否则将阻塞,直到有空间保存用户的数据。
- 发送一个长度为0的数据也是合理的,sendto将返回0,对于数据报socket,它将传送一个0长度的数据报,只包含协议首部,没有用户数据。
- sendto也可用于面向连接的socket,这种情况,to和tolen将被忽略,sendto等价于send。
recvfrom
接收数据并得到数据的源地址,通常用于无连接socket,并且已经绑定了本地地址
int WSAAPI recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR* from,int FAR * fromlen);
- return
调用成功返回接收的字节数
返回0时,对于面向连接的socket表示对方正常关闭了连接,对于无连接socket,表示接收到了对方发送的0长度的数据报
发生错误时返回SOCKET_ERROR - s : 标识了一个socket
- buf 接收数据的缓冲区
- len buf的长度
- flag 规定了调用的方式
影响recvfrom函数的行为, 可以是0或者下表中一个或者多个常量值的逻辑或 - from : 可选,成功时返回源地址
- fromlen : from地址缓冲区的长度
当from非空时,发出数据的主机地址被复制到from中,fromlen是复制到from中地址的长度
说明:
- 无连接socket接收数据时常用recvfrom,把输入队列中的第一个数据报复制到buf中,如果数据报的长度比缓冲区大,将只把数据报前面len字节的数据复制到buf中,多余的数据会丢失,recvfrom产生错误码WSAEMSGSIZE
- 返回0, 表示接收到了一个0长度的数据报,因为UDP是无连接的,没有关闭连接操作。
- 面向连接socket,如类型为SOCK_STREAM,接收数据通常使用recv,但也可以使用recvfrom,除了最后两个参数被忽略外,它与recv功能是一样的。
面向连接,当协议中的数据大于recvfrom提供的缓冲区长度时,系统将len字节的数据复制到buf中,其他的数据仍然保留在队列缓冲区中,不会像无连接socket一样把超过的数据抛弃。 - 调用recvfrom时,如果系统中没有数据
- 在阻塞模式下,recvfrom一直等待,直到接收到了数据或发生了错误
- 非阻塞模式下,会返回错误SOCKET_ERROR,调用WSAGetLastError得到的错误码是WSAEWOULDBLOCK。
可以用select或WSAAsyncSelect确定什么时候数据到达
closesocket
关闭socket,释放占用的资源
int WSAAPI closesocket(SOCKET s);
- return
成功返回0
发送错误时返回SOCKET_ERROR - s 套接口描述符,标识了要关闭的socket
说明:
- 程序成功调用socket后,应该总是调用closesocket,释放地址信息,抛弃排队的数据,取消正在进行的阻塞操作或异步调用,应用程序将不会收到通知消息或事件信号
- 关闭之后,不能再使用这个套接口描述符,也不能再把它作为send、recv等函数的参数,错误码是WSAENOTSOCK
- 对于使用UDP协议的socket,没有连接,关闭操作非常简单,只是释放本地资源,并立即返回
- 使用TCP协议的socket,通信双方要维护连接,关闭操作受选项SO_LINGER的影响,情况比较复杂应用程序想控制关闭的行为,需要创建一个struct linger结构
struct linger{ u_short l_onoff; // 打开或者关闭选项 u_short l_linger; // 延迟时间,单位s }
- SO_LINGER起作用,要调用setsockopt函数,把l_onoff设置为非零值,并且l_linger为0或者是期望的以秒为单位的超时值
- 要让SO_DONTLINGER生效,结构中的l_onoff应该为0
- SO_LINGER和SO_DONTLINGER是互斥的,一个生效后,另一个自然就无效了
- SO_DONTLINGER是socket的默认行为
这种情况调用closesocket会立即返回,如果队列中还有尚未发送的数据,底层协议不会立即关闭socket,而是先发送数据,数据发送完成后转换到关闭状态,双方都同意关闭后,两端才真正关闭socket,这被称作“优美关闭”。
造成的影响是底层协议在一段时间内并不关闭socket,释放相关的资源,因此应用程序在这段时间内也不能使用这个socket。
Daytime程序
Daytime是互联网上的一个标准协议,在RFC867中有详细的描述
Daytime服务器只是简单地把当前的日期和时间作为一个字符串发送给客户端,它既可以使用TCP,也可以使用UDP协议,知名端口号是13。
客户端
UDP协议是不可靠的,发送的数据可能丢失,为了尽量避免这种情况,最好发送多次
setsockopt(SO_RCVTIMEO)设置接收数据的超时值 --> 防止客户端一直阻塞在recvfrom函数
设置接收的超时选项,在指定的时间内收不到数据时,recvfrom函数会返回SOCKET_ERROR,错误码为WSAETIMEDOUT
#include <stdio.h> #include <winsock2.h> #define DAYTIME_DEF_PORT 13 /* Daytime默认端口 */ #define DAYTIME_BUF_SIZE 64 /*缓冲区的大小 */ #define DAYTIME_DEF_COUNT 2 /* 发送的次数 */ int main(int argc, char **argv) { WSADATA wsaData; /* 初始化WinSock资源 */ WSAStartup(WINSOCK_VERSION, &wsaData); /* Daytime socket句柄 */ SOCKET time_soc = 0; struct sockaddr_in peer_addr, serv_addr; /* 接收超时, 2秒 */ int timeout = 2000; int i, result, send_len, addr_len = sizeof(serv_addr); char *dest = "127.0.0.1", *send_data = "Hello, Daytime!"; char recv_buf[DAYTIME_BUF_SIZE]; /* 服务器地址 */ if (argc == 2) dest = argv[1]; send_len = strlen(send_data); /* 服务器地址 */ serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(DAYTIME_DEF_PORT); serv_addr.sin_addr.s_addr = inet_addr(dest); if (serv_addr.sin_addr.s_addr == INADDR_NONE) { printf("[Daytime] invalid address\r\n"); return -1; }; /* 创建Daytime使用的socket */ time_soc = socket(AF_INET, SOCK_DGRAM, 0); result = setsockopt(time_soc, SOL_SOCKET, SO_RCVTIMEO,(char*)&timeout, sizeof(timeout)); for (i = 0; i < DAYTIME_DEF_COUNT; i++) { result = sendto(time_soc, send_data, send_len, 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); result = recvfrom(time_soc, recv_buf, DAYTIME_BUF_SIZE, 0, (struct sockaddr *)&peer_addr, &addr_len); if (result >= 0) { recv_buf[result] = 0; printf("[Daytime] recv: \"%s\", from %s\r\n", recv_buf, inet_ntoa(peer_addr.sin_addr)); } } closesocket(time_soc); WSACleanup(); return 0; }
服务器端
函数recvfrom的返回值大于等于0,表示收到了对方的数据
#include <stdio.h> #include <time.h> #include <winsock2.h> #define DAYTIME_DEF_PORT 13 /* Daytime默认端口 */ #define DAYTIME_BUF_SIZE 64 /* 缓冲区的大小 */ int main(int argc, char **argv) { WSADATA wsaData; /* 初始化WinSock资源 */ WSAStartup(WINSOCK_VERSION, &wsaData); /* 服务器的socket句柄 */ SOCKET srv_sock = 0; /* Daytime服务器地址 */ struct sockaddr_in srv_addr, clnt_addr; /* 客户端地址 */ unsigned short port = DAYTIME_DEF_PORT; int result, addr_len = sizeof(srv_addr); char *time_str, recv_buf[DAYTIME_BUF_SIZE]; time_t now_time; if (argc == 2) /* 端口 */ port = atoi(argv[1]); /* 创建Daytime使用的socket */ srv_sock = socket(AF_INET, SOCK_DGRAM, 0); /* Daytime服务器地址 */ srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(port); srv_addr.sin_addr.s_addr = INADDR_ANY; result = bind(srv_sock, (struct sockaddr *)&srv_addr, addr_len); if (result == SOCKET_ERROR) { printf("[Daytime] bind error : %d", WSAGetLastError()); closesocket(srv_sock); return -1; } printf("[Daytime] The server is running ... ...\r\n"); while (1) { result = recvfrom(srv_sock, recv_buf, DAYTIME_BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &addr_len); if (result >= 0) { recv_buf[result] = 0; printf("[Daytime] recv: \"%s\", from %s\r\n", recv_buf, inet_ntoa(clnt_addr.sin_addr)); now_time = time(0); /* 得到当前时间 */ time_str = ctime(&now_time); /* 向客户端发送当前的日期和时间字符串 */ sendto(srv_sock, time_str, strlen(time_str), 0, (struct sockaddr *)&clnt_addr, addr_len); } } closesocket(srv_sock); WSACleanup(); return 0; }
说明
Devc++ 添加winsock链接库