对于之前的TCP协议而言,他是可靠的字节流传输,而在socket编程中,在不需要保证数据传输正确安全的情况下。或者由用户自己完成传输确认情况/服务端客户端自己实现数据传输。套接字编程也提供了UDP协议的方法。
基于UDP(不是面向连接)的socket编程,分为客户端和服务器端。
客户端的流程如下:
(1)创建套接字(socket)
(2)和服务器端进行通信(sendto)
(3)关闭套接字
因为在socket编程中,UDP是针对数据报的数据传输,所以socket专门定义了UDP所使用的函数接口。
sendto函数:指向一指定目的地发送数据,sendto()适用于发送未建立连接的UDP数据包
返回值为整型,如果成功,则返回发送的字节数,失败则返回-1,并SOCKET_ERROR。
sockfd: 套接字 也就是我们所建立的sock文件描述符
buf: 待发送数据的缓冲区 在客户端中定义的栈存储数据缓冲区
len: 缓冲区长度 确认的发送长度。
flags:调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式:阻塞 可以定义为不同的发送方式,有各种不同的发送方式。0为阻塞发送。
addr:(可选)指针,指向目的套接字的地址。调用存储我们所连接套接字的信息,
addr:lenaddr所指地址的长度。
服务器端的流程如下:
(1)创建套接字(socket)
(2)将套接字绑定到一个本地地址和端口上(bind)
(3)用返回的套接字和客户端进行通信(recvfrom)
(4)返回,等待另一个客户请求。
(5)关闭套接字。
recvfrom函数:本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
sockfd:标识一个已连接套接口的描述字。同上
buf:接收数据缓冲区。同上
len:缓冲区长度。同上
flags:调用操作方式。同上
src_addr:(可选)指针,指向装有源地址的缓冲区。——输出型参数。同上
addrlen:(可选)指针,指向from缓冲区长度值。——输入输出型参数。同上
返回值为整型,如果成功,则返回接收到的字节数,失败则返回-1和SOCKET_ERROR。
总结:其实对于UDP协议的服务端和客户端而言,他主要是满足UDP协议的数据快速传输,服务端和客户端快速访问,没有TCP的3次握手过程,他只需要确认数据发送出去,不需要保证数据的传输是否成功,所以对于这样的UDP服务器端客户端而言。他是比较简单的,服务端不需要进入监听和连接的建立没有accept(),listen(),客户端不需要connnet();
实现机制比较简单,但是有时候为了保证数据传输的有效,我们需要实现一些其他的机制,这个以后再更新。
我们先看一下简单版本的实现代码:
//server端
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(const char* proc)
{
printf("%s [ip][port]\n");
}
int main(int argc,char* argv[])
{
if(argc!=3){
Usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
return 2;
}
int _port=atoi(argv[2]);
char* _ip=argv[1];
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(1);
}
char buf[1024];
struct sockaddr_in remote;
socklen_t len=sizeof(remote);
while(1)
{
ssize_t _s=recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&remote,&len);
if(_s>0)
{
buf[_s]='\0';
printf("client:[ip:%s][port:%d] %s",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port),buf);
}
else if(_s==0){
printf("client close");
break;
}
else
{
break;
}
}
close(sock);
return 0;
}
//client端
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(const char* proc)
{
printf("%s[ip][port]",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3){
Usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
return 2;
}
int _port=atoi(argv[2]);
char* _ip=argv[1];
struct sockaddr_in client;
client.sin_family=AF_INET;
client.sin_port=htons(_port);
client.sin_addr.s_addr=inet_addr(_ip);
char buf[1024];
while(1)
{
ssize_t _s=read(0,buf,sizeof(buf)-1);
if(_s>0){
buf[_s]='\0';
}
_s=sendto(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,sizeof(client));
}
return 0;
}
下面我们来讲一下关于端口的问题,这涉及到上一个TCP中所遗留下来的问题:
首先我们需要了解一下端口是什么:
(Port)相当于一种数据的传输通道。用于接受某些数据,然后传输给相应的服务,而电脑将这些数据处理后,再将相应的恢复通过开启的端口传给对方。一般每一个端口的开放的偶对应了相应的服务,要关闭这些端口只需要将对应的服务关闭就可以了。
对于端口而言,我们可以将它理解为任何一个在当我们的应用程序:进程需要进行通信的时候,我们进行网络上的消息传输需要识别,但是在同一IP下,我们无法具体识别是哪个进程进行提取信息。所以就出现了端口。
每一种东西出现是解决某一种或某一类问题而出现的。在通讯过程中IP是固定只有那么多的,所以为了解决通讯地址的重复出现了端口的概念。就相当于电话的分机号码一样。
TCP和UDP采用16b的端口号来识别应用程序。那么这些端口号是如何选择的呢?
TCP和UDP采用16b的端口号来识别应用程序。那么这些端口号是如何选择的呢?
服务器一般都是通过知名端口号来识别的。例如,对于TCP/IP实现来说,每个FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(普通文件传输协议)服务器的UDP端口号都是69。任何TCP/IP实现所提供的服务都用知名的1~1 023之间的端口号。这些知名端口号由Internet号分配机构(Internet Assigned Numbers Authority, IANA)来管理。到1992年为止,知名端口号介于1~255之间。256~1 023之间的端口号通常都是由UNIX系统占用,以提供一些特定的UNIX服务,也就是说,提供一些只有UNIX系统才有的,而其他操作系统可能不提供的服务。现在IANA管理1~1 023之间所有的端口号。
Internet扩展服务与UNIX特定服务之间的一个差别就是telnet和rlogin,它们二者都允许通过计算机网络登录到其他主机上。telnet是采用端口号为23的TCP/IP标准,且几乎可以在所有操作系统上进行实现。相反,rlogin最开始时只是为UNIX系统设计的(尽管许多非UNIX系统现在也提供该服务),因此在20世纪80年代初,它的端口号为513,客户端通常对它所使用的端口号并不关心,只须保证该端口号在本机上是唯一的即可。客户端口号又称做临时端口号(即存在时间很短暂),这是因为它通常只是在用户运行该客户程序时才存在,而服务器则只要主机开着,其服务就运行。
大多数TCP/IP实现给临时端口分配1 024~5 000之间的端口号。大于5 000的端口号是为其他服务器预留的(Internet上并不常用的服务)。大多数Linux系统的文件/etc/services都包含了人们熟知的端口号。
需要注意的一点是:
计算机之间依照互联网传输层TCP/IP协议不同的协议通信,都有不同的对应端口。所以,利用短信(datagram)的UDP,所采用的端口号码不一定和采用TCP的端口号码一样。以下为两种通信协议的端口列表链接:
然后在上一篇博文中我们留下的一个问题就是server端主动关闭后,如何避免2MSL的TIME_WAIT.
其实就是采取了端口复用的规则,也就是socket的一个服务端的地址:端口能够重复使用,跟IP复用是同一个道理,都是为了解决重复相关的问题。
解决上述的问题使server端避免进入2MSL时间在结束之后就能尽快的再次进行全网的监听呢?可以使用setsockopt函数,设置socket描述符的选项SO_REUSEADDR为1,其作用是可以允许重用本地端口号和地址:
也就是修改套接字的描述符:
函数原型:
这就是关于套接字选项的设置,以后我会专门写一篇博客来说明套接字选项。现在我们只需要知道如何避免2MSL
函数参数中,
sockfd是创建出的socket文件描述符;
level被指定为SOL_SOCKET;
optname是对相应协议模块的描述,也就是设置sockfd描述符的选项,这里就设置为SO_REUSEADDR,允许重用本地地址和端口;
optval和optlen用于访问选项值,也就是optname,这里的值应设置为1;
函数成功返回0,失败返回-1并置相应的错误码;
在创建监听socket和绑定函数bind之间插入函数setsockopt。