一、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)结构字段
结构定义头文件 | #include <netinet/in.h> |
---|---|
sa_family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | |
AF_LOCAL:UNIX协议 | |
AF_LINK:链路地址协议 | |
AF_KEY:密钥套接字(socket) |
2.数据存储优先顺序
计算机数据存储有两种字节优先顺序:大端(高位字节优先)和小端(低位字节优先)。Internet上采用大端传输,因此需要对两个字节存储的优先顺序进行转化。Linux提供了以下四个函数:
所需头文件 | #include <netinet/in.h> |
---|---|
函数原型 | 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)函数格式
所需头文件 | #include <arpa/inet.h> | |
---|---|---|
函数原型 | int inet_pton(int family,const char *strptr,void *addrptr) | |
函数传入值 | family | AF_INET:IPv4协议 |
AF_INET6:IPv6协议 | ||
strptr:要转化的值 | ||
addrptr:转化后的地址 | ||
函数返回值 | 成功:0 | |
出错:-1 |
所需头文件 | #include <arpa/inet.h> | |
---|---|---|
函数原型 | 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)函数格式
所需头文件 | #include <netdb.h> |
---|---|
函数原型 | struct hostent *gethostbyname(const char *hostname) |
函数传入值 | hostname:主机名 |
函数返回值 | 成功:hostent类型指针 |
出错:-1 | |
注:调用该函数首先对hostent结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置则默认为IPv4地址类型。 |
所需头文件 | #include <netdb.h> |
---|---|
函数原型 | int getaddrinfo(const char *node,const char *service,const struct addrinfo *hints,struct addrinfo **result) |
函数传入值 | node:网络地址或者网络主机名 |
service:服务名或十进制的端口号字符串 | |
hints:服务线索 | |
result:返回结果 | |
函数返回值 | 成功:0 |
出错:-1 |
结构体头文件 | #include <netdb.h> |
---|---|
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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
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\n",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)函数格式
所需头文件 | #include <sys/socket.h> | |
---|---|---|
函数原型 | 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 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int bind(int sockfd,struct sockaddr *my_addr,int addrlen) |
函数传入值 | sockfd:套接字描述符 |
my_addr:本地地址 | |
addrlen:地址长度 | |
函数返回值 | 成功:0 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int listen(int sockfd,int backlog) |
函数传入值 | sockfd:套接字描述符 |
backlog:请求队列中允许的最大请求数,大多数系统默认值为5 | |
函数返回值 | 成功:0 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen) |
函数传入值 | sockfd:套接字描述符 |
addr:客户端地址 | |
addrlen:地址长度 |
|
函数返回值 | 成功:客户端文件描述符 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int connect(int sockfd,struct sockaddr *serv_addr,int addrlen) |
函数传入值 | socket:套接字描述符 |
serv_addr:服务器端地址 | |
addrlen:地址长度 | |
函数返回值 | 成功:0 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int send(int sockfd,const void *msg,int len,int flags) |
函数传入值 | sockfd:套接字描述符 |
msg:指向要发送数据的指针 | |
len:数据长度 | |
flags:一般为0 | |
函数返回值 | 成功:发送的字节数 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | int recv(int sockfd,void *buf,int len,unsigned int flags) |
函数传入值 | sockfd:套接字描述符 |
buf:存放接收数据的缓冲区 | |
len:数据长度 | |
flags:一般为0 | |
函数返回值 | 成功:接收的字节数 |
出错:-1 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | 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 |
所需头文件 | #include <sys/socket.h> |
---|---|
函数原型 | 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 <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#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\n",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!\n");
/*调用listen()函数,创建未处理请求的队列,MAX_QUE_CONN_NM=请求队列中允许
的最大请求数,大多数系统默认值为5*/
if(listen(sockfd,MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("Listening...\n");
/*调用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\n",buf);
close(sockfd);
exit(0);
}
客户端:
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#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\n");
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);
}