目录
- 什么是socket
- socket基本操作API函数
- socket服务器的实现
- 附录:网络字节序与主机字节序
什么是socket
socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现,socket是操作系统抽象出一个概念,连接传输层与应用层的上层应用如图中所示套接字(socket)是一个抽象层,继承了Linux下“万物皆文件”的思想,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合
socket基本操作API函数
1. socket()函数
函数原型及所需的头文件
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
(1)作用:socket()函数用来创建一个socket描述符,socket描述符类似于普通的文件描述符,可以进行读写操作,一个socket描述符唯一指定一个socket
(2)三个参数:
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议
(3)返回值:socket()调用成功时,返回socket描述符;调用失败时返回一个负值,可调用strerror(errno)函数查看错误原因
2. setsockopt()
作用:在网络socket服务器编程时避免端口占用,并发服务器中要重复使用端口,通过如下语句设置端口重用(单线程服务器中一般不考虑端口重用所以不需要使用此语句)
int on=1;
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
3. bind()函数
所需的头文件与
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)功能:将socket与IP和端口绑定
(2)参数:第一个参数即为socket()返回的socket描述符;第二个参数为const struct sockaddr*类型的指针,sockaddr为通用套接字,其定义为
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 存放socket地址值
};`
因使用的地址协议族不同,实际使用的套接字多种多样,如sockaddr_in,sockaddr_in6,sockeaddr_un(分别对应IPv4,IPv6以及Unix域通信协议),赋值时都须通过强制类型转换转换为sockaddr类型传参;第三个参数为对应地址的长度,需要注意的是,1024以下的端口是系统占用的端口,调用时需要root或sudo权限,一般避免使用1024以下的端口
(3)返回值:如无错误发生,则bind()返回0。否则的话,将返回-1
4. listen()函数
int listen(int sockfd, int backlog);
开始监听套接字
sockfd是socket描述符,backlog为相应socket可以在内核里排队的大连接个数
5. accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
(1)作用:用来等待客户端的连接,默认为阻塞函数,只有客户端调用connect()函数时才会触发accept()函数
(2)参数:sockfd:由socket创建的socket描述符
addr:用于返回客户端的协议地址,这个地址里包含客户端的IP和端口等信息
addrlen:返回的客户端协议地址的长度
(3)返回值:由内核生成的一个全新的描述字(fd),代表与返回客户端的TCP相连,可以通过对其进行读写操作向客户端读写
6. connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)作用:客户端向服务器端进行连接
(2)参数:sockfd:客户端的socket描述符
addr:要连接的服务器端的地址信息,这个地址里包含客户端的IP和端口等信息
addrlen:服务器协议地址的长度
(3)返回值:成功返回0,失败返回-1
socket服务器的实现
客户端与服务器通信的示意图如下
由此可见在单线程服务器中API函数的执行流程是:socket()->bind()->listen()->accept()->close()
下面给出一个简单的socket服务器的实现代码
int main()
{
int sockfd =-1;
int client_fd =-1;
struct sockaddr_in serv_addr;
struct sockaddr_in cli_addr;
// int serv_port =6666;
// char* serv_ip ="127.0.0.1";
socklen_t cliaddr_len;
int rv =-1;
char buf[1024];
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
printf("create sockfd failure: %s\n",strerror(errno));
return -1;
}
printf("create sockfd successfully! \n");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = INADDR_ANY;
rv=bind(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
if(rv<0)
{
printf("bind failure: %s\n",strerror(errno));
return -2;
}
printf("bind successfully \n");
listen(sockfd,BACKLOG);
while(1)
{
printf("\n%d start waiting and accepting new client connect... \n",sockfd);
client_fd=accept(sockfd,(struct sockaddr *)&cli_addr,&cliaddr_len);
if(client_fd<0)
{
printf("connect failure: %s\n",strerror(errno));
return -3;
}
printf("accept new client with fd \n");
memset(buf,0,sizeof(buf));
rv=read(client_fd,buf,sizeof(buf));
if(rv<0)
{
printf("connect to client error: %s\n",strerror(errno));
close(client_fd);
continue;
}
else if(rv==0)
{
printf("client connect to server get disconnected \n");
close(client_fd);
continue;
}
printf("get %d Bytes from client \n",rv);
rv=write(client_fd,MSG_STR,strlen(MSG_STR));
if(rv<0)
{
printf("write %d Bytes to client error: %s\n",rv,strerror(errno));
close(client_fd);
}
printf("write %d Bytes to client successfully! \n",rv);
sleep(1);
close(client_fd);
}
close(sockfd);
return 0;
}
记录一下程序编写时遇到的错误:
1.
编译器提示此条目为库函数头文件未正确包含或库函数名称拼错
2.字符串求长度用strlen函数不能用sizeof函数
需要注意的地方:
1.127.0.0.1为本机IP;INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思
2.accept函数最后一个参数应先在主函数中定义socklen_t类型变量cliaddr_len接收connect函数的传参,再传地址&cliaddr_len传参给accept函数
3.定义的结构体和缓冲区要先调用memset清空,避免出现随机值
4.
如下面的代码中所示,对IPv4协议套接字结构体进行初始化时要注意其中的成员sin_addr为内嵌结构体,要访问其内部成员s_addr对其赋值
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
同时赋值时要注意调用htons()和htonl()进行网络字节序与主机字节序的转换,那么什么是网络字节序与主机字节序,将在下面介绍
附录:网络字节序与主机字节序
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序, 一个字节的数据没有顺序的问题了
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端 b)
Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端
网络字节序: 4个字节的32 bit值以下面的次序传输:首先是0 ~ 7bit,其次8~ 15bit,然后16~
23bit,最后是24~31bit。
这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序
所以,在将一 个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian
可以通过调用两个函数htons()和htolnl()分别用来将端口和IP地址转换成网络字节序,这两个函数名中的h表示host,
n表示network, s表示short(2字节/1
6位),表示long(4字节/32位)。因为端口号是16位的,所以我们用htons(把端口号从主机字节序转换成网络字节序,而IP地址是32位的,
所以我们用htonl()函数把IP地址从主机字节序转换成网络字节序
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
上述两个函数只适用ipv4,下面两个函数兼容ipv4和ipv6 inet_pton()和inet_ntop(),因IPv4地址逐渐枯竭,推荐使用这两个函数
inet_pton函数所需头文件:
#include <sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
函数原型:
int inet_pton(int af, const char *src, void *dst);
(1)参数:第一个参数af是地址簇;src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
(2)返回值:如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0
(3)调用伪代码:
char IPdotdec[20]; //存放点分十进制IP地址
struct in_addr s; // IPv4地址结构体
inet_pton(AF_INET, IPdotdec, (void *)&s);