TCP
和 UDP
TCP
追求的是可靠的传输数据, UDP
追求的则是快速的传输数据以下例子以
*nix
平台的便准为例,因为Windows
平台需要考虑额外的加载问题,稍作添加就能在 Windows 平台上运行
UDP
这是一个十分简洁的连接方式,假设有两台主机进行通信,一台只发送,一台只接收。
接收端:
int sock; /* 套接字 */
socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
char mess[15];
char get_mess[GET_MAX]; /* 后续版本使用 */
struct sockaddr_in recv_host, send_host;
/* 创建套接字 */
sock = socket(PF_INET, SOCK_DGRAM, 0);
/* 把IP 和 端口号信息绑定在套接字上 */
memset(&recv_host, 0, sizeof(recv_host));
recv_host.sin_family = AF_INET;
recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
/* 进入接收信息的状态 */
recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
/* 接收完成,关闭套接字 */
close(sock);
上述代码省略了许多必要的 错误检查 ,在实际编写时要添加
代码解释:
htonl
和 htons
两个函数的使用涉及到 大端小端问题, 这里不叙述,需要记住的是在网络编程时一定要使用这种函数将必要信息转为 大端表示法 。(struct sockaddr *)
这个强制转换是为了参数的必须,但不会出错,因为 sizeof(struct sockaddr_in) == sizeof(struct sockaddr)
具体可以查询相关信息,之所以这么做是为了方便编写套接字程序的程序员。发送端:
int sock;
const char* mess = "Hello Server!";
char get_mess[GET_MAX]; /* 后续版本使用 */
struct sockaddr_in recv_host;
socklen_t addr_len;
/* 创建套接字 */
sock = socket(PF_INET, SOCK_DGRAM, 0);
/* 绑定 */
memset(&recv_host, 0, sizeof(recv_host));
recv_host.sin_family = AF_INET;
recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
recv_host.sin_port = htons(6000);
/* 发送信息 */
/* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
/* 完成,关闭 */
close(sock);
上述代码是发送端。
代码解释:
inet_addr
函数是用于将字符串格式的 IP地址 转换为 大端表示法的 地址类型,即 s_addr
的类型 in_addr_t
inet_ntoa
用于将 in_addr_t
类型转为字符串,但是使用时一定要记住及时拷贝返回值 char addr[16]; recv_host.sin_addr.s_addr = inet_addr("127.0.0.1"); strcpy(addr, inet_ntoa(recv_host.sin_addr.s_addr));从上述代码看出, UDP
协议的使用十分简洁,几乎就是 创建套接字->准备数据->装备套接字->发送/接收->结束
其中,都没有连接的操作,但是实际上这是为了方便 UDP
随时和 不同的主机 进行通信所默认的设置,如果需要和相同主机一直通信呢?
此中的原由暂时不需要知道,记录方法,即长时间使用 UDP
和同一主机通信时,可以使用 connect
函数来进行优化自身。此时 假设两台主机的实际功能一致,既接收也发送
发送端:
/* 前方高度一致,将 bind函数替换为 */
connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 将对方的 IP地址和 端口号信息 注册进UDP的套接字中)
while(1) /* 循环的发送和接收信息 */
{
size_t read_len = 0;
/* 原先使用的 sendto 函数,先择改为使用 write 函数, Windows平台为 send 函数 */
write(sock, mess, strlen(mess)); /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
get_mess[read_len-1] = '\0';
printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
}
/* 后方高度一致 */
接收端:
/* 前方一致, 添加额外的 struct sockaddr_in send_host; 并添加循环,构造收发的现象*/
while(1)
{
size_t read_len = 0;
char sent_mess[15] = "Hello Sender!"; /* 用于发送的信息 */
sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
mess[read_len-1] = '\0';
printf("In Sever like Host Recvive From other Host : %s\n", mess);
}
/* 后方高度一致 */
/*
* 之所以只在接收端使用 connect 的原因,便在于我们模拟的是 客户端-服务器 的模型,而服务器的各项信息是不会随意变更的
* 但是 客户端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能总是固定的,所以只能
* 保证 在客户端 部分注册了 服务器 的各类信息,而不能在 服务器端 注册 客户端 的信息。
* 当然也有例外,例如你就想这个软件作为私密软件,仅供两个人使用, 且你有固定的 IP地址,那么你可以两边都connect,但是
* 一定要注意,只要有一点信息变动,这个软件就可能无法正常的收发信息了。
*/
UDP
似乎并没有 connect
的使用必要,但是实际上还是有用到的地方。*nix
的 API 来说,sendto
和 write
的区别十分明显,便是一个需要在参数中提供目标主机的各类信息,而后者则不需要提供。同样的道理recvfrom
和read
也是如此。以上是 UDP 的一些简单说明,入门足矣,并未详细叙述某些 函数 的具体用法,而是用实际例子来体现。 在 记录 TCP 之前,还是需要讲一个函数 shutdown
shutdown
与 close(closesocket)
首先要知道,网络通信一般而言是双方的共同进行的,换而言之就是双向的,一个方向只用来发送消息,一个方向只用来读取消息。
这就导致了,在结束套接字通信的时候,需要关闭两个方向的通道(暂时叫它们通道),那同时关闭不行吗?可以啊
close(sock); // closesocket(sock); FOR Windows PlatForm
就是这么干的,同时断开两个方向的连接。发送端:
/* 假设有一个 TCP 的连接,此为客户端 */
write(sock, "Thank you", 10);
close(sock); // 写完直接关闭通信
接收端:
/* 此为服务器 */
/* 首先关闭写流 */
shutdown(sock_c, SHUT_WR);
read(sock_c, get_mess, GET_MAX);
printf("Message : %s\n", get_mess);
close(sock_c);
close(sock_s); // 关闭两个套接字是因为 TCP 服务器端的需要,后续会记录
代码解释
shutdown
函数的作用就是 可选择的关闭那个方向的输出
int shutdown(int sock, int howto);
sock
代表要操作的套接字howto
有几个选择
SHUT_RD
SHUT_WR
SHUT_RDWR
SD_RECEIVE
SD_SEND
SD_BOTH