一、socket概述
1.socket定义
socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。每一个socket都用一个半相关描述符{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整形的socket描述符,随后的连接建立、数据传输等操作都通过socket来实现。
2.socket类型
常见的socket有3中类型:
流式socket(SOCK_STREAM) |
流式套接字提供可靠地、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
数据报socket(SOCK_DGRAM) |
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并不保证是可靠、无差错的。它使用数据报协议UDP。
原始socket |
原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
二、地址及顺序处理
1.地址结构相关处理
(1)用来保存socket信息的两个数据结构:sockaddr和sockaddr_in:
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号*/
};
struct sockaddr_in
{
short int sa_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero(8); /*填充0以保持与struct sockaddr同样大小*/
};
(2)结构字段
sa_family可选值
结构定义头文件 |
#include
sa_family |
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX协议
AF_LINK:链路地址协议
AF_KEY:密钥套接字(socket)
2.数据存储优先顺序
计算机数据存储有两种字节优先顺序:大端(高位字节优先)和小端(低位字节优先)。Internet上采用大端传输,因此需要对两个字节存储的优先顺序进行转化。Linux提供了以下四个函数:
htons等函数语法要点
所需头文件 |
#include
函数原型 |
uint16_t htons(uint16_t host16bit)
uint32_t htonl(uint32_t host32bit)
uint16_t ntohs(uint16_t net16bit)
uint32_t ntohl(uint32_t net32bit)
函数传入值 |
host16bit:主机字节序的16位数据
host32bit:主机字节序的32位数据
net16bit:网络字节序的16位数据
net32bit:网络字节序的32位数据
函数返回值 |
成功:返回要转换的字节序
出错:-1
注:h表示host,n表示network,s表示short,l表示long,通常16位端口号用s表示,32位IP地址用l表示。 |
3.地址格式转化
(1)函数说明
通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。其中inet_pton()函数是将点分十进制地址映射为二进制地址,inet_ntop()是将二进制地址映射为点分十进制地址。
(2)函数格式
inet_pton函数语法要点
所需头文件 |
#include
函数原型 |
int inet_pton(int family,const char *strptr,void *addrptr)
函数传入值 |
family
AF_INET:IPv4协议
AF_INET6:IPv6协议
strptr:要转化的值
addrptr:转化后的地址
函数返回值 |
成功:0
出错:-1
inet_ntop函数语法要点
所需头文件 |
#include
函数原型 |
int inet_ntop(int family,void *addrptr,char *strptr,size_t len)
函数传入值 |
family
AF_INET:IPv4协议
AF_INET6:IPv6协议
addrptr:转化后的地址
strptr:要转化的值
len:转化后值的大小
函数返回值 |
成功:0
出错:-1
4.名字地址转化
(1)函数说明
在Linux中,同样有一些函数可以实现主机名和地址的转化,最为常见的有gethostbyname()、gethostbyaddr()和getaddrinfo()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()是将IP地址转化为主机名,getaddrinfo()实现自动识别IPv4地址和IPv6地址。
gethostbyname()和gethostbyaddr()都涉及一个hostent的结构体,如下所示:
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*地址类型*/
int h_length; /*地址字节长度*/
char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/
}
getaddrinfo()函数涉及一个addrinfo的结构体,如下所示:
struct addrinfo
{
int ai_flags; /*AI_PASSIVE,AI_CANONNAME*/
int ai_family; /*地址族*/
int ai_socktype; /*socket类型*/
int ai_protocol; /*协议类型*/
size_t ai_addrlen; /*地址字节长度*/
char *ai_canonname; /*主机名*/
struct sockaddr *ai_addr; /*socket结构体*/
struct addrinfo *ai_next; /*下一个指针链表*/
}
(2)函数格式
gethostbyname函数语法要点
所需头文件 |
#include
函数原型 |
struct hostent *gethostbyname(const char *hostname)
函数传入值 |
hostname:主机名
函数返回值 |
成功:hostent类型指针
出错:-1
注:调用该函数首先对hostent结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置则默认为IPv4地址类型。 |
getaddrinfo()函数语法要点
所需头文件 |
#include
函数原型 |
int getaddrinfo(const char *node,const char *service,const struct addrinfo *hints,struct addrinfo **result)
函数传入值 |
node:网络地址或者网络主机名
service:服务名或十进制的端口号字符串
hints:服务线索
result:返回结果
函数返回值 |
成功:0
出错:-1
addrinfo结构体常见常数
结构体头文件 |
#include
ai_flags: |
AI_PASSIVE:该套接口是用作被动地打开
AI_CANONNAME:通知getaddrinfo函数返回主机的名字
ai_family: |
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_UNSPEC:IPv4或IPv6均可
ai_socktype: |
SOCK_STREAM:字节流套接字socket(TCP)
SOCK_DGRAM:数据报套接字socket(UDP)
ai_protocol: |
IPPROTO_IP:IP协议
IPPROTO_IPV4:IPv4协议
IPPROTO_IPV6:IPv6协议
IPPROTO_UDP:UDP
IPPROTO_TCP:TCP
注:
(1)通常服务器端在调用getaddrinfo()之前,ai_flags设置AI_PASSIVE,用于bind()函数(用于端口和地址的绑定),主机名 nodename通常会设置为NULL。
(2)客户端调用getaddrinfo()时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(端口)则应该不为空。
(3)即使不设置ai_flags为AI_PASSIVE,取出的地址也可以被绑定,很多程序中ai_flags直接设置为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确绑定。
|
(3)使用实例
/* getaddrinfo.c */
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
struct addrinfo hints,*res = NULL;
int rc;
memset(&hints,0,sizeof(hints));
hints.ai_flags = AI_CANONNAME; /*通知getaddrinfo函数返回主机的名字*/
hints.ai_family = AF_UNSPEC; /*IPv4和IPv6均可*/
hints.ai_socktype = SOCK_DGRAM; /*数据报套接字socket*/
hints.ai_protocol = IPPROTO_UDP; /*UDP协议*/
rc = getaddrinfo("localhost1",NULL,&hints,&res);
if(rc != 0)
{
perror("getaddrinfo");
exit(1);
}
else
{
printf("Host name is %s
",res->ai_canonname); /*主机名*/
}
exit(0);
}
三、socket基础编程
(1)函数说明
socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,函数说明如下:
socket() |
该函数用于建立一个socket连接,可指定socket类型等信息。在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。
bind() |
该函数是用于将本地IP地址绑定到端口号,若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。
listen() |
在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
accept() |
服务端程序调用listen()函数创建等待队列之后,调用accept()函数等待并接收客户端的连接请求。它通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。
connect() |
该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。
send()和recv() |
这两个函数分别用于发送和接收数据,可以用在TCP中,也可以用在UDP中。当用在UDP时,可以在connect()函数建立连接之后再用。
sendto()和recvfrom() |
这两个函数的作用与send()和recv()函数类似,也可以用在TCP和UDP中。当用在TCP时,后面的几个与地址有关参数不起作用,函数作用等同于send()和recv();当用在UDP时,可以用在之前没有使用connect()的情况下。这两个函数可以自动寻找指定地址并进行连接。
(2)函数格式
socket()函数语法要点
所需头文件 |
#include
函数原型 |
int socket(int family,int type,int protocol)
函数传入值 |
family:
协议族
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX域协议
AF_ROUTE:路由套接字(socket)
AF_KEY:密钥套接字(socket)
type:
套接字类型
SOCK_STRAM:字节流套接字socket
SOCK_DGRAM:数据报套接字socket
SOCK_RAW:原始套接字socket
protocl:0(原始套接字除外)
函数返回值 |
成功:非负套接字描述符
出错:-1
bind()函数语法要点
所需头文件 |
#include
函数原型 |
int bind(int sockfd,struct sockaddr *my_addr,int addrlen)
函数传入值 |
sockfd:套接字描述符
my_addr:本地地址
addrlen:地址长度
函数返回值 |
成功:0
出错:-1
listen()函数语法要点
所需头文件 |
#include
函数原型 |
int listen(int sockfd,int backlog)
函数传入值 |
sockfd:套接字描述符
backlog:请求队列中允许的最大请求数,大多数系统默认值为5
函数返回值 |
成功:0
出错:-1
accept()函数
所需头文件 |
#include
函数原型 |
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
函数传入值 |
sockfd:套接字描述符
addr:客户端地址
addrlen:地址长度
函数返回值 |
成功:客户端文件描述符
出错:-1
connect()函数语法要点
所需头文件 |
#include
函数原型 |
int connect(int sockfd,struct sockaddr *serv_addr,int addrlen)
函数传入值 |
socket:套接字描述符
serv_addr:服务器端地址
addrlen:地址长度
函数返回值 |
成功:0
出错:-1
send()函数语法要点
所需头文件 |
#include
函数原型 |
int send(int sockfd,const void *msg,int len,int flags)
函数传入值 |
sockfd:套接字描述符
msg:指向要发送数据的指针
len:数据长度
flags:一般为0
函数返回值 |
成功:发送的字节数
出错:-1
recv()函数语法要点
所需头文件 |
#include
函数原型 |
int recv(int sockfd,void *buf,int len,unsigned int flags)
函数传入值 |
sockfd:套接字描述符
buf:存放接收数据的缓冲区
len:数据长度
flags:一般为0
函数返回值 |
成功:接收的字节数
出错:-1
sendto()函数语法要点
所需头文件 |
#include
函数原型 |
int sendto(int sockfd,const void *msg,int len,unsigned int flags,const struct sockaddr *to,int tolen)
函数传入值 |
sockfd:套接字描述符
msg:指向要发送数据的指针
len:数据长度
flags:一般为0
to:目的机的IP地址和端口号信息
tolen:地址长度
函数返回值 |
成功:发送的字节数
出错:-1
recvfrom()函数语法要点
所需头文件 |
#include
函数原型 |
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen)
函数传入值 |
sockfd:套接字描述符
buf:存放接收数据的缓冲区
len:数据长度
flags:一般为0
from:源主机的IP地址和端口号信息
tolen:地址长度
函数返回值 |
成功:接收的字节数
出错:-1
(3)使用实例
服务端:
/* server.c */
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 4321 //端口号
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;//sockfd=文件描述符
char buf[BUFFER_SIZE];
/*建立socket连接*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建TCP套接字
{
perror("socket");
exit(1);
}
printf("Socket id = %d
",sockfd);
/*设置sockaddr_in结构体中相关参数*/
server_sockaddr.sin_family = AF_INET; //sin_family=地址族,AF_INET=IPv4
server_sockaddr.sin_port = htons(PORT);//sin_port=端口号,htons()主机字节序转化
server_sockaddr.sin_addr.s_addr = INADDR_ANY;//s_addr=IP地址,INADDR_ANY指0.0.0.0,表示本地地址
bzero(&(server_sockaddr.sin_zero),8);//填充0以保持与struct sockaddr同样大小
int i=1;//允许重复使用本地地址与套接字进行绑定
if((setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i))) < 0)//用于任意类型、任意状态套接口的设置选项值
{
perror("setsockopt");
exit(1);
}
/*sockfd = 标识一个套接口的描述字
level = 选项定义的层次:支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
optname = 需设置的选项
optval = 指针,指向存放选项待设置的新值的缓冲区
optlen = optval缓冲区长度*/
/*绑定函数bind(),server_sockaddr = 本地地址,sizeof(struct sockaddr) = 地址长度*/
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) ==-1)
{
perror("bind");
exit(1);
}
printf("Bind success!
");
/*调用listen()函数,创建未处理请求的队列,MAX_QUE_CONN_NM=请求队列中允许
的最大请求数,大多数系统默认值为5*/
if(listen(sockfd,MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("Listening...
");
/*调用accept()函数,等待客户端的连接,client_sockaddr = 客户端地址,地址长度*/
if((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)) ==-1)
{
perror("accept");
exit(1);
}
/*调用recv()函数接收客户端的请求*/
memset(buf,0,sizeof(buf));
if((recvbytes = recv(client_fd,buf,BUFFER_SIZE,0)) == -1)//返回接收的字>节数
{
perror("recv");
exit(1);
}
printf("Received a message:%s
",buf);
close(sockfd);
exit(0);
}
客户端:
/*client.c*/
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc,char *argv[])
{
int sockfd,sendbytes;
char buf[BUFFER_SIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if(argc < 3)
{
fprintf(stderr,"USAGE:./client Hostname(or ip address) Test
");
exit(1);
}
/*地址解析函数*/
if((host = gethostbyname(argv[1])) == NULL)
{
perror("gethostbyname");
exit(1);
}
memset(buf,0,sizeof(buf));
sprintf(buf,"%s",argv[2]);
/*创建socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(1);
}
/*设置sockaddr_in 结构体中相关参数*/
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
/*调用connet函数主动发起对服务器端的连接*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)//serv_addr = 服务器端地址
{ //sizeof(struct sockaddr) = 地址长度
perror("connect");
exit(1);
}
/*发送消息给服务器端*/
if((sendbytes = send(sockfd,buf,strlen(buf),0)) == -1)
{
perror("send");
exit(1);
}
sleep(5);
close(sockfd);
exit(0);
}