socket编程API
创建套接字–用于网络传输
- 在Linux操作系统中,要实现socket通信,通信双方都需要建立各自的socket对象,在应用层,socket对象是一种特殊的文件描述符,可以使用I/O系统调用(read/write)来读写,socket()函数用于创建socket,其函数声明如下:
#include <sys/socket.h>
int socket(int domain, //协议域
//AF_INET(IPv4)
//AF_INET6(IPv6)
//AF_UNIX(本地套接字)
int type, //套接字类型
//流式SOCK_STREAM (TCB)
//数据报SOCK_DGRAM (UDP)
//原始SOCK_RAM
int protocol //协议类型,一般写0
);
返回值:套接字描述符(文件描述符),简称为套接字(可读可写)
- 此函数如果执行成功,将返回一个打开的socket文件描述符,此时,该socket对象没有绑定任何IP信息,还不能进行通信,如果执行失败,返回-1
绑定本地IP地址与端口
- 使用socket()创建的socket是没有任何约束的,它没有与具体的端口号相关联,在服务器端,需要使用bind函数绑定该套接字。bind()函数声明如下:
#include <sys/socket.h>
int bind(int sockfd, //刚才打开的套接字(用于绑定本地IP信息的文件描述符)
const struct sockaddr *addr, //套接字地址(必须含有IP地址和端口号)
ocklen_t addrlen //地址的长度(一般有sizeof求得)
);
- 第二个参数addr:是一个指向sockaddr结构的指针,标识绑定的本地地址信息,如果是IP信息,则要求IP地址必须为本地IP地址,端口必须为一个未占用的本地端口。sockaddr数据结构定义如下:
struct sockaddr{
sa_family_t sa_family; /* address family,AF_xxx */ //协议簇
char sa_data[14]; /* 14 bytes of protocol address */ //协议地址
}
- struct sockaddr只是提供地址类型规范,根据不同的应用,sockaddr需要选用不同的类型。
- 例如IPv4网络通信,socket需要与本机可用的IP地址和端口号绑定,因此,sockaddr结构体应该选用一下定义:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
//struct in_addr为32位IP地址,具体定义如下:
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
- 其中,端口对任何一个socket都是唯一的,唯一的端口号可以区分本地唯一的应用程序,因此,socket所绑定的端口号不能与其他应用程序重复,小于1024的端口号为系统保留,用户应用程序不能随便使用
setsockopt()–获取或者设置与某个套接字关联的选项,函数定义如下
#include <sys/socket.h>
int setsockopt(int sockfd, //将要被设置或者获取选项的套接字
int level, //选项所在的协议层
int optname, //需要访问的选项名
void *optval, //指向包含新选项值的缓冲
socklen_t *optlen //现选项的长度
);
- level:SOL_SOCKET(通用套接字选项)、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项)…..
- optname:SO_REUSEADDR(允许重用本地地址)、SO_REUSEPORT(允许重用本地端口)、…..
- optval:若想在关闭套接字后继续重用该socket,可设置为1.
监听网络
- 绑定了IP地址和端口号的socket对象还不能进行TCP方式通信,因为当前还没有能力监听网络请求。因此,对于面向连接的应用来说,服务器端需要调用listen函数使该socket对象监听网络。仅由TCP服务器调用。将一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,函数声明如下:
#include <sys/socket.h>
int listen(int sockfd, //刚才创建的套接字
int backlog //等待队列的最大个数
);
- listen函数将绑定的socket文件描述符变为监听套接字,此时服务器已经准备好接收客户端的连接请求了
客户端发起连接
- 如果服务器已经监听网络,且客户端创建了socket对象,则客户端可以使用connect()函数与服务器端建立连接了。connect()函数声明如下:
#include<sys/socket.h>
int connect(int sockfd, //socket返回的文件描述符
const struct sockaddr *servaddr,//目的主机地址(IP和端口号)
socklen_t addrlen); //该地址的长度
- 如果执行成功,此函数将于地址为servaddr的服务器建立连接,并返回0,失败返回-1
服务器接收连接
- 如果服务器监听到客户端的连接请求,则需要调用accept()函数接受请求,如果没有监听到客户端的连接请求,则此函数处于阻塞状态。accept()函数声明如下:
#include <sys/socket.h>
int accept(int sockfd, //监听网络后的socket文件描述符
struct sockaddr *addr, //哪个客户端连接我,我就记录此客户端的地址()
socklen_t *addrlen //传入是告诉接收方分配的空间多大,传出是告诉对方实际占用空间多大
);
返回值:新的套接字,用于与客户端进行通信(传出客户端的地址和端口号)
- 如果连接成功,返回新的文件描述符以标识该连接,从而使原来的文件描述符可以继续监听网络等待新的连接,这样便可以实现多客户端。如果执行失败,返回-1
读/写socket对象
- read函数是负责从socket对象中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。
- write函数将buf中的nbytes字节内容写入到socket对象中,成功返回写的字节数,失败返回-1.并设置errno变量。
- 函数声明如下:
#include <unistd.h>
ssize_t write(int fd,
const void *buf,
size_t count
);
ssize_t read(int fd,
void *buf,
size_t count
);
TCP发送/接收数据
- Linux提供send()和recv()函数来专门实现面向连接的socket对象读写操作。
- send()函数用来发送数据,函数声明如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, //目标socket对象
const void *buf, //欲发送数据位置
size_t len, //数据大小
int flags //置0,则与write()行为一致;MSG_PEEK,查看外来消息,系统不丢弃看到的数据;……
);
- 如果执行成功,则返回发送数据的大小,失败返回-1
- recv()函数用来接收数据,其将从socketfd中读取n个字节到buf中,函数声明如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 如果执行成功,则返回接收数据的大小,失败返回-1
关闭socket对象
- 通信结束后,关闭socket对象,使用close()函数
#include<unistd.h>
int close(int fd);
获取socket本地及对端信息
- 使用getsockname()函数将获取一个套接字(这个套接字至少完成了绑定本地IP地址)的本地地址。如果成功返回0,失败返回-1。函数声明如下:
#include <sys/socket.h>
int getsockname(int sockfd, //想要读取信息的socket文件描述符
struct sockaddr *addr, //存储地址的内存空间的地址
socklen_t *addrlen //存储地址的内存空间的大小
);
- 函数使用示例:
struct sockaddr_in test;
getsockname(fd,(struct sockaddr*)&test,&size);
printf("ip=%s,port=%d\n",inet_ntoa(test.sin_addr),ntohs(test.sin_port));
主机字节顺序与网络字节顺序的转
- htonl:将主机的unsignedlong值转换成网络字节顺序(32位)(一般主机跟网络上传输的字节顺序是不通的,分大小端),函数返回一个网络字节顺序的数字
- 记忆这类函数,主要看前面的n和后面的hl。n代表网络,h代表主机host,l代表long的长度,还有相对应的s代表16位的short
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IPv4地址转换
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//将点分十进制的字符串转换为32位网络字节顺序的IP信息
in_addr_t inet_addr(const char *cp);
//将点分十进制的字符串转换为32为主机字节序的IP信息
in_addr_t inet_network(const char *cp);
//将点分十进制的字符串IP信息准换成32位的网络字节序
int inet_aton(const char *cp, struct in_addr *inp);
//将32位的网络字节序的IP信息转换为点分十进制的字符串形式
char *inet_ntoa(struct in_addr in);
简易服务器端-客户端实现
- 上面我们介绍了socket编程的常用接口,下面我们将应用上面介绍的接口实现一个简单的服务端-客户端模型
服务器端
- 创建套接字(socket) -> 绑定(bind) -> 监听(listen) -> 等待客户端连接(accept) -> 读取客户端发送过来的数据(read) -> 回复客户端(write) -> 关闭(close)
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
int main(int argc,char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
if(argv[1])
addr.sin_addr.s_addr = inet_addr(argv[1]);
else
addr.sin_addr.s_addr = INADDR_ANY; //否则设置本机任意地址
int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1)
{
perror("bind");
return -1;
}
printf("本地 ip = %s and port = %d 绑定成功...\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
ret = listen(fd,10);
if(ret == -1)
{
perror("listen");
return -1;
}
printf("等待客户端连接...\n");
//服务器接收客户端的连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int newfd = accept(fd,(struct sockaddr*)&cliaddr,&len);
if(newfd == -1)
{
perror("accept");
return -1;
}
printf("ip = %s and port = %d 的客户端已连接我方服务器...\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
//已经有客户端连接到我了,提示客户端输入消息,读取客户端写的数据并发送写回给客户端
//一个客户端可以和服务端进行多次交互
while(1)
{
char buf[1024]={};
ssize_t r = recv(newfd,buf,sizeof(buf),0);
if(r == -1)
{
perror("recv");
return -1;
}
printf("%s",buf);
//将服务器读到的信息写回给客户端
r = write(newfd,buf,sizeof(buf));
if(r == -1)
{
perror("write");
return -1;
}
}
close(newfd);
close(fd);
return 0;
}
客户端
- 创建套接字(socket) -> 连接服务器(connect) -> 向服务器端写入数据(write) -> 读取服务器端的回复(read) -> 关闭(close)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
int main(int argc,char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
//和服务端进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],(struct in_addr*)&addr.sin_addr.s_addr);
int r = connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(r == -1)
{
perror("connect");
return -1;
}
printf("已连接到 ip = %s port = %d 的服务器,准备通讯\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
char buf[1024]={};
while(fgets(buf,1024,stdin))
{
if(!strncasecmp(buf,"quit",4))
{
printf("本客户端要退出了...\n");
break;
}
ssize_t r = send(fd,buf,sizeof(buf),0);
if(r == -1)
{
perror("send");
return -1;
}
r = read(fd,buf,sizeof(buf));
if(r == -1)
{
perror("read");
return -1;
}
printf("%s",buf);
memset(buf,0x00,sizeof(buf));
}
close(fd);
printf("已和 ip = %s and port = %d 的服务器断开连接...\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
return 0;
}
- 执行结果如下:
多进程版本的服务端-客户端模型
- 对上面模型进行改进,使其达到一个服务器可以同时与多个客户端进行叫交互
- 针对于此,我们通过多进程来实现多个客户端并行的情况,父进程阻塞等待客户端的来连接,子进程来与服务器进行交互
- 实现代码如下:
服务端代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void handle_cli(int newfd,struct sockaddr_in cliaddr)
{
while(1)
{
char buf[1024]={};
ssize_t r = recv(newfd,buf,sizeof(buf),0);
if(r == -1)
{
perror("recv");
exit(0);
}
if(!strncasecmp(buf,"quit",4))
{
printf("客户端 ip = %s and port = %d 已从本端断开...\n",
inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port));
close(newfd);
exit(0);
}
printf("客户端 ip = %s and port = %d 说:%s",inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port),buf);
r = write(newfd,buf,sizeof(buf));
if(r == -1)
{
perror("write");
exit(0);
}
}
}
int main(int argc,char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1)
{
perror("bind");
return -1;
}
//绑定本地端口号成功
printf("绑定本地 ip = %s and port = %d 成功...\n",inet_ntoa((struct in_addr)addr.sin_addr),ntohs(addr.sin_port));
ret = listen(fd,10);
if(ret == -1)
{
perror("listen");
return -1;
}
printf("等待客户端的连接...\n");
//通过父子进程来实现服务器与客户端一对多的模型(可以多次进行通信)
//让父进程阻塞等待客户端的到来,子进程处理与服务器的交互
while(1)
{
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int newfd = accept(fd,(struct sockaddr*)&cliaddr,&len);
if(newfd == -1)
{
perror("accept");
return -1;
}
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork()");
return -1;
}else if(pid > 0)//父进程
{
close(newfd);
continue;
}else if(pid == 0)//子进程
{
close(fd);
printf("客户端 ip = %s and port = %d 已与我方服务器连接,准备通讯...\n",
inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port));
handle_cli(newfd,cliaddr);
return -1;
}
close(newfd);
}
close(fd);
return 0;
}
客户端代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1)
{
perror("connect");
return -1;
}
printf("成功与服务器 ip = %s and port = %d 连接,准备进行通讯...\n",
inet_ntoa((struct in_addr)addr.sin_addr),ntohs(addr.sin_port));
char buf[1024] = {};
while(fgets(buf,1024,stdin))
{
if(!strncasecmp(buf,"quit",4))
{
send(fd,buf,sizeof(buf),0);
close(fd);
return -1;
}
int r = send(fd,buf,sizeof(buf),0);
if(r == -1)
{
perror("send");
return -1;
}
r = read(fd,buf,sizeof(buf));
if(r == -1)
{
perror("read");
return -1;
}
printf("%s",buf);
memset(buf,0x00,sizeof(buf));
}
close(fd);
return 0;
}
- 程序运行结果如下所示: