前言
socket(套接字)是linux下进程间通信的一种方式,通常使用C-S(客户端-服务端)的方式通信,它可以是同一主机下的不同进程间通信或者不同主机的进程通信。
socket是夹在应用层和TCP/UDP协议层间的软件抽象,向应用层开发人员提供API接口,向下隐藏协议层的具体细节,大大方便了我们开发人员。很多平台都实现了BSD scoket标准scoket接口,增强了可移植性。
在进行socket网络编程之前,有必要对计算机网络有个大概的了解,这里推荐一篇博文,链接如下:TCP/IP协议族之TCP、UDP协议详解
1. socket概述
1.1 表示方法
Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面加上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点所确定。
1.2 socket主要类型
① 流套接字(SOCK_STREAM):用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。
② 数据报套接字(SOCK_DGRAM):提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
③ 原始套接字(SOCK_RAW):原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。
2. socket-API接口
2.1 socket函数
socket函数用于初始化创建一个通信端点,调用成功返回一个socket描述符,类似于open函数的文件描述符;
可以使用close函数来关闭socket,释放占用的资源。
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol)
参数:
domain:指示通信协议族;
AF_UNIX或 AF_LOCAL:Local communication本地通信;
AF_INET:IPv4 Internet protocols,就是我们通常用的ipv4地址;
AF_INET6:IPv6 Internet protocols,ipv6地址;
AF_NETLINK:Kernel user interface device;
其它;
type:指定套接字的类型;
SOCK_STREAM:提供有序的、可靠的、双向的、基于连接的字节流,默认TCP协议;
SOCK_DGRAM:固定长度的、无连接的、不可靠的报文传递,默认UDP协议;
SOCK_RAW:允许应用程序访问网络层的原始数据包,原始套接字;
protocol:通常设置为0,表示为给定的通信域和套接字类型选择默认协议。
返回值:
成功:返回非负数socket描述符;
失败:返回 -1。
2.2 bind函数
bind函数通常用于服务端,将服务端的socket文件与网络中的进程地址(IP地址+端口号)绑定;
指向sockaddr_in的结构体指针也可以指向sockaddr的结构体,所以经常使用sockaddr_in替代。
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数:
sockfd:socket描述符;
addr:地址(ip地址+端口号);
addrlen:第二个参数addr长度;
返回值:
成功:返回 0;
失败:返回 -1。
struct sockaddr { sa_family_t sa_family; char sa_data[14];
}struct sockaddr_in {sa_family_t sin_family; //协议族in_port_t sin_port; //端口号struct in_addr sin_addr; // IP 地址unsigned char sin_zero[8];
};
2.3 listen函数
listen函数调用进入监听状态,通常用于服务端,服务端调用listen函数进入监听状态,等待客户端的连接请求。
参数backlog表明连接等待队列的上限,如果该服务端已经连接上一个客户端,之后其它客户端的连接请求将进入连接等待队列,同时该队列会有个上限值,不可能无限大。
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
int listen(int sockfd, int backlog)
参数:
backlog:连接等待队列上限值;
返回值:
成功:返回 0;
失败:返回 -1。
2.4 accept函数
accept()函数用于获取客户端的连接请求并建立连接,通常用于服务端;它是一个阻塞函数,如果没有客户端的连接请求,则会阻塞等待;accept()函数返回的套接字连接到调用connect()的客户端,服务端通过该套接字与客户端进行数据交互。
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
参数:
sockfd:socket()函数返回的描述符;
addr:struct sockaddr指针变量,是一个传出参数,用于存放客户端的连接信息,可设为NULL,表示不关心客户端信息;
addrlen:表明传出参数addr的字节长度;可设为NULL;
返回值:
成功:返回一个新的socket描述符;
失败:返回 -1 。
2.5 connect函数
通常用于客户端,调用connect函数请求与服务端连接;常说的三次握手就是由connect触发的。
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数:
sockfd:调用socket生成的描述符;
addr:指定服务端的IP地址以及端口号等信息;
addrlen:表明addr信息长度;
返回值:
成功:返回 0;
失败:返回 -1。
2.6 发送函数
2.6.1 send函数
send函数通常用于TCP数据包的发送。
send()成功返回只能表示数据包已经发送出去了,并不能代表接收端已经接收到数据了。
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
参数:
sockfd:socket函数返回值;
buf:发送的数据缓冲区;
len:发送的数据大小;
flags:标志位,通常设为0;
MSG_DONTWAIT:允许非阻塞操作;
MSG_MORE:延迟发送数据包允许写更多数据;
其它......
返回值:
失败:返回 -1;
2.6.2 sendto函数
常用于UDP数据发送,因为UDP是无连接的,因此需要指定目的地址。
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)参数:
dest_addr:数据发送的目的地址;
addrlen:目的地址长度;
2.7 接收函数
2.7.1 recv函数
用于TCP数据的接收;阻塞模式下,直到有数据才返回。
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
参数:
buf:数据接收缓冲区;
flags:一般设为0;
MSG_DONTWAIT:启动非阻塞操作;
MSG_PEEK:返回数据包内容而不真正取走数据包;
其它......
返回值:
成功:返回接收的数据长度;
失败:返回 -1。
2.7.2 recvfrom函数
一般用于UDP数据的接收。
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)参数:
src_addr:指向数据源端地址。
2.8 IP地址格式转化
2.8.1 inet_ntop函数
用于二进制形式字符串转为十进制形式字符串。
头文件:
#include <arpa/inet.h>
函数原型:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)
参数:
af:为AF_INET(ipv4)或AF_INET6(ipv6);
src:需要转化的源字符串;一般为struct in_addr结构体的对象;
dst:转换生成的字符串存放的缓冲区;
size:缓冲区的大小;
返回值:
成功:返回指向dst的指针;
失败:返回NULL。
代码示例:
#include <stdio.h>
#include <arpa/inet.h>int main()
{struct in_addr src_addr;char dst_addr[32];src_addr.s_addr = 0x6401a8c0;inet_ntop(AF_INET,&src_addr,dst_addr,sizeof(dst_addr));printf("dst addr:%s\n",dst_addr);return 0;
}
2.8.2 inet_pton函数
用于十进制形式字符串转为二进制形式字符串,返回值为大端存储。
头文件:
#include <arpa/inet.h>
函数原型:
int inet_pton(int af, const char *src, void *dst)
返回值:
成功:返回1;
格式无效:返回0;
失败:返回-1。
struct in_addr {__be32 s_addr;
};
示例代码:
#include <stdio.h>
#include <arpa/inet.h>#define IP_ADDR "192.168.1.100"
int main()
{struct in_addr dst_addr;inet_pton(AF_INET,IP_ADDR,&dst_addr);printf("dst addr:%x\n",dst_addr.s_addr);return 0;
}
2.9 存储顺序
host:主机字节序(小端存储);
network:网络字节序(大端存储)。
2.9.1 主机转网络存储顺序
头文件:
#include <arpa/inet.h>
函数原型:
uint32_t htonl(uint32_t hostlong); //32位IP地址
uint16_t htons(uint16_t hostshort); //16位端口号
INADDR_ANY指定地址为0.0.0.0地址,表示监听所有的IP地址
2.9.2 网络转主机存储顺序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
3. socket工作流程
3.1 TCP工作流程
3.2 UDP工作流程
4. TCP通信测试代码
4.1 服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define PORT 6666int main()
{int socket_fd,fd;int ret;char send_buff[64] = "Data was send by server."; //连接上发给客户端的数据char recv_buff[64];char client_ip[32];struct sockaddr_in client_msg;int msg_len = sizeof(client_msg);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);socket_fd = socket(AF_INET,SOCK_STREAM,0);if(socket_fd < 0){perror("socket");exit(0);}ret = bind(socket_fd,(struct sockaddr *)&server_addr,sizeof(server_addr));if(ret == -1){perror("bind");close(socket_fd);exit(0);}ret = listen(socket_fd,32);if(ret == -1){perror("listen");close(socket_fd);exit(0);}printf("listen...\n");fd = accept(socket_fd,(struct sockaddr *)&client_msg,&msg_len);inet_ntop(AF_INET,&client_msg.sin_addr.s_addr,client_ip,sizeof(client_ip));printf("client ip addr:%s,port:%d\n",client_ip,client_msg.sin_port);send(fd,send_buff,sizeof(send_buff),0);while(1){memset(recv_buff,0,sizeof(recv_buff));recv(fd,recv_buff,sizeof(recv_buff),0); //阻塞接收客户端的数据printf("recv buff :%s\n",recv_buff);if(strncmp("quit",recv_buff,4) == 0){printf("socket closer.\n");close(fd);close(socket_fd);exit(0);}}return 0;
}
4.2 客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main(int argc,char *argv[])
{int socket_fd;int ret;char send_buff[] = "From client datas.";char recv_buff[64];struct sockaddr_in client_msg;if(argc < 3){exit(0);}printf("ip:%s,port:%s\n",argv[1],argv[2]);client_msg.sin_family = AF_INET;inet_pton(AF_INET, argv[1], &client_msg.sin_addr); //传入的ip地址client_msg.sin_port = htons(atoi(argv[2])); //传入的端口号htonl(client_msg.sin_addr.s_addr);socket_fd = socket(AF_INET,SOCK_STREAM,0);if(socket_fd == -1){perror("socket");exit(0);}printf("socket_fd :%d\n",socket_fd);ret = connect(socket_fd,(struct sockaddr *)&client_msg,sizeof(client_msg)); //发起连接if(ret == -1){perror("connect");close(socket_fd);exit(0);}recv(socket_fd,recv_buff,sizeof(recv_buff),0);printf("recv buff : %s\n",recv_buff);while(1){memset(send_buff,0,sizeof(send_buff));printf("client data input: ");fgets(send_buff,sizeof(send_buff),stdin); //从键盘输入数据信息send(socket_fd,send_buff,sizeof(send_buff),0);if(strncmp("quit",send_buff,4) == 0){close(socket_fd);exit(0);}}return 0;
}
4.3 测试结果
在Ubuntu下打开两个shell终端,分别运行服务端程序和客户端程序。
客户端传入服务端ip地址和端口号的参数,然后向服务端发送数据信息测试,结果如下图。