Socket套接字 由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将
TCP/IP
协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了 Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h
中。
socket
其实就是一个网络通信的接口,或者说是标准,我们通过使用这个接口就能进行网络通信。在进行通信之前,我们需要确定对方主机的地址,也就是对方主机的 IP
地址;由于网络通信的消息是发送给对方主机上的某一个具体的进程的,比如用 QQ
发送消息给对方,对方也是用 QQ
这个应用接收消息的,所以我们需要指定将这个消息发送给对方主机上 QQ
这个应用,我们通过指定 QQ
这个进程对应的 Port
端口号 完成。
注意:socket
翻译过来的意思是 插座,有插座的话那么就存在 插头,插头 看作 客户端,插座 看成 服务端,将 插头 插到 插座 上的这个过程就类似于 客户端和服务端通信建立连接的过程。肯定是先要有 插座,才会有插头;实际应用中,应该是先启动客户端等待客户端的请求连接,再启动客户端建立连接。
字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指电脑内存中或在数字通信链路中,组成多字节的字的字节的排列顺序。
字节序存在 大端序(主机字节序) 和 小端序(网络字节序)。
// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制)
内存低地址位 内存的高地址位
--------------------------------------------------------------------------->
小端: 0xff 0x01 0x5c 0xab
大端: 0xab 0x5c 0x01 0xff
注意: 对于 int
, long
这种 4个字节,8个字节是一个整体的类型才会存在字节序问题,单字节类型是不会存在字节序问题的,比如字符串就不会有字节序问题。
为什么要介绍这个字节序的问题?
在 socket 网络通信过程中,收发的数据,端口,IP地址都是大端序的。而我们主机平时使用的是小端序,所以我们需要在通信之前将对应的数据做相应的转换。
大端序 和 小端序对应的转换函数:
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
// 将一个短整形从主机字节序(小端序) -> 网络字节序(大端序)
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 将一个短整形从网络字节序(大端序) -> 主机字节序(小端序)
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);
IP
地址IP
地址实际本质上是一个整数,但是在形式上我们用一个 “点分十进制” 的字符串来描述,比如本地地址是 "127.0.0.1"
。
我们可以通过下面的函数,将 主机字节序(小端序)的 IP
地址(字符串) 转换为 网络字节序(大端序,整数):
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
参数:
af
:IP
地址族(包括:AF_INET
IPv4格式的 IP
地址;AF_INET6
IPv6格式的 IP
地址);src
:IP
地址,一个点分十进制的字符串,比如 "127.0.0.1"
;dst
:传出参数,对应的 src
被转换成 网络字节序(大端序) 之后,就将结果写到了 *dst
这个指针所指的内存处;返回值:
通过下面这个函数,可以将 网络字节序(大端序)的 IP
地址(整数) 转换成 主机字节序(小端序)点分十进制的 IP
地址(字符串):
#include
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af
:IP
地址族(包括:AF_INET
IPv4格式的 IP
地址;AF_INET6
IPv6格式的 IP
地址);src
:网络字节序的整型(大端序) IP
地址;dst
:传出参数,将 src
转换成的 主机字节序(小端序)的 点分十进制的 IP
地址;size
:修饰 dst
,表示 dst
指向的内存最多可以存入 size
个字节的数据;返回值:
IP
字符串;NULL
;sockaddr
结构// 在写数据的时候不好用
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
使用套接字通信函数需要引入头文件
:
// 创建一个套接字
int socket(int domain, int type, int protocol);
参数:
domain
:使用的地址族协议。AF_INET
,使用 IPv4 格式的 IP
地址;AF_INET6
,使用 IPv6 格式的 IP
地址;type
:SOCK_STREAM
,使用流式传输的协议,TCP
协议;SOCK_DGRAM
,使用报文传输的协议,UDP
协议;protocal
:一般写 0 0 0,使用默认协议。SOCK_STREAM
默认使用的是 TCP
协议;SOCK_DGRAM
默认使用的是 UDP
协议;返回值:
该函数执行成功的返回值是一个 文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信就是基于这个文件描述符来完成的。
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:用于监听的文件描述符,是通过调用 socket()
函数的返回值;addr
:要绑定的 IP
地址 和 端口信息 需要在这个结构体中初始化,IP
地址 和 端口信息需要转换为 网络字节序(大端序);addrlen
:参数 addr
指向内存的大小,即 sizeof (struct addr)
;返回值:
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
参数:
sockfd
:文件描述符,通过调用 socket()
函数的返回值,在监听之前必须先绑定 bind()
;backlog
:能够同时处理的最大连接数,最大值为 128 128 128;返回值:
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd
:监听的文件描述符;addr
:建立连接的客户端的信息;addrlen
:用于存储 addr
指向内存的大小;返回值:
注意: 这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞在这里;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
参数:
fd
:用于通信的文件描述符,调用 accept()
函数的返回值;buf
:要发送的的数据;len
:要发送数据的长度;flags
:特殊的属性,一般不使用,默认为 0 0 0;返回值:
注意: 如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
参数:
fd
: 通信的文件描述符, accept()
函数的返回值;buf
: 传入参数, 要发送的字符串;len
: 要发送的字符串的长度;flags
: 特殊的属性, 一般不使用, 指定为 0 0 0;返回值:
len
是相等的;// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 通信的文件描述符, 通过调用 socket()
函数就得到;addr
: 存储了要连接的服务器端的地址信息: IP
和 端口,这个 IP
和端口也需要转换为网络字节序(大端序)然后再赋值;addrlen
: addr
指针指向的内存的大小 sizeof(struct sockaddr)
;返回值:
TCP
通信TCP
是一个面向连接的、安全的、流式传输协议,是传输层的协议。
TCP
通信的大致流程如下:
lfd
int lfd = socket();
lfd
和本地端口 和 IP
地址绑定 bind();
listen();
cfd
(用于通信的),没有客户端连接就一直阻塞 int cfd = accept();
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
close();
在服务端,有两种文件描述符:
用于 监听 的文件描述符:
accept()
就能建立新的连接;用于 通信 的文件描述符:
文件描述符对应的内存结构:
用于监听的文件描述符:
accept()
函数, 这个函数会检测监听文件描述符的读缓冲区:用于通信的文件描述符:
客户端和服务端都有用于通信的文件描述符;
发送数据:调用 write() / send()
函数,将数据写到内核:
接收数据:调用 read() / recv()
函数, 从内核读数据:
在单线程的情况下,客户端用于通信的文件描述符只有一个,没有用于监听的文件描述符。
int cfd = socket();
IP
地址 和 端口号connect();
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
close();
此时只能是一个客户端 跟 一个服务端进行通信。
服务端代码 , server.c
:
#include
#include
#include
#include
#include
int main(){
//1. 创建监听的套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1){ //监听套接字创建失败
perror("socket");
return -1;
}
//2. 绑定本地的 IP : Port
/**
* 初始化 saddr 绑定 IP 和 Port 信息
*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; //IPv4
saddr.sin_port = htons(9999); //Port 需要转换成大端序
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd,(struct sockaddr*)(&saddr),sizeof(saddr));
if(ret == -1){ //绑定失败
perror("bind");
return -1;
}
//3. 设置监听
ret = listen(lfd , 128);
if(ret == -1){ //监听失败
perror("listen");
return -1;
}
//4. 阻塞并等待客户端的连接
struct sockaddr_in caddr;
int caddr_len = sizeof(caddr);
int cfd = accept(lfd,(struct sockaddr*)(&caddr),&caddr_len);
if(cfd == -1){ //连接失败
perror("accept");
return -1;
}
/**
*连接建立成功,打印客户端的 IP 和 Port 信息
注意:需要将信息由大端序 转为 小端序
*/
char ip[32];
printf("客户端的IP : %s , 端口Port : %d\n",inet_ntop(AF_INET,&caddr.sin_addr.s_addr,ip,sizeof(ip)),ntohs(caddr.sin_port));
//5. 开始进行通信
char buf[1024];
while(1){
//接受数据
int len = recv(cfd,buf,sizeof(buf),0);
if(len > 0){ //还有数据
printf("Client say : %s\n",buf);
send(cfd,buf,sizeof(buf),0);
}
else if(len == 0){ //说明客户端已经断开了连接
printf("客户端已经断开了连接...!\n");
break;
}
else{ //len == -1 说明读取数据失败
perror("recv");
break;
}
}
//6.关闭文件描述符
close(lfd);
close(cfd);
return 0;
}
客户端代码,client.c
:
#include
#include
#include
#include
#include
int main(){
//1. 创建通信的套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){ //监听套接字创建失败
perror("socket");
return -1;
}
//2. 连接服务器
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; //IPv4
saddr.sin_port = htons(9999); //Port 需要转换成大端序
inet_pton(AF_INET,"10.0.8.14",&saddr.sin_addr.s_addr);
int ret = connect(fd,(struct sockaddr*)(&saddr),sizeof(saddr));
if(ret == -1){ //连接失败
perror("connect");
return -1;
}
//3. 开始进行通信
int num = 0;
while(1){
char buf[1024];
//发送数据
sprintf(buf,"hello socket communication ... %d \n",num++);
send(fd,buf,strlen(buf) + 1,0);
//接收数据
memset(buf,0,sizeof buf);
int len = recv(fd,buf,sizeof(buf),0);
if(len > 0){ //还有数据
printf("Server say : %s\n",buf);
}
else if(len == 0){ //说明服务端已经断开了连接
printf("服务端已经断开了连接...!\n");
break;
}
else{ //len == -1 说明读取数据失败
perror("recv");
break;
}
sleep(1); // 每隔 1s 再发一次数据
}
//6.关闭文件描述符
close(fd);
return 0;
}
实现效果:
对于每一个客户端的连接,服务端都生成一个子线程去跟客户端进行通信。这样就可以多个客户端,连接同一个服务端。
我们需要将服务端的代码改成多线程的版本。
server_mutl_thread.c
:
#include
#include
#include
#include
#include
#include
//信息结构体
struct sock_info{
struct sockaddr_in addr;
int cfd;
};
struct sock_info infos[512];
void* work(void* arg);
int main()
{
// 0. 初始化信息结构体数组
size_t n = sizeof(infos) / sizeof(infos[0]);
for(size_t i = 0;i < n;i++){
bzero(&infos[i] , sizeof(infos[i]));
infos[i].cfd = -1;
}
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{ // 监听套接字创建失败
perror("socket");
return -1;
}
// 2. 绑定本地的 IP : Port
/**
* 初始化 saddr 绑定 IP 和 Port 信息
*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; // IPv4
saddr.sin_port = htons(9999); // Port 需要转换成大端序
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr *)(&saddr), sizeof(saddr));
if (ret == -1)
{ // 绑定失败
perror("bind");
return -1;
}
// 3. 设置监听
ret = listen(lfd, 128);
if (ret == -1)
{ // 监听失败
perror("listen");
return -1;
}
// 4. 阻塞并等待客户端的连接
int len = sizeof(struct sockaddr_in);
while (1)
{
struct sock_info* pinfo = NULL;
for(size_t i = 0;i < n;i++){
if(infos[i].cfd == -1){
pinfo = &infos[i];
break;
}
}
int cfd = accept(lfd, (struct sockaddr *)(&(pinfo->addr)), &len);
pinfo->cfd = cfd;
if (cfd == -1)
{ // 连接失败
perror("accept");
break;
}
//创建子线程
pthread_t tid;
pthread_create(&tid,NULL,work,pinfo);
//分离 子线程 跟 父线程
pthread_detach(tid);
}
close(lfd);
return 0;
}
void *work(void *arg)
{
/**
*连接建立成功,打印客户端的 IP 和 Port 信息
注意:需要将信息由大端序 转为 小端序
*/
struct sock_info * pinfo = (struct sock_info*) arg;
char ip[32];
printf("客户端的IP : %s , 端口Port : %d\n", inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port));
// 5. 开始进行通信
char buf[1024];
while (1)
{
// 接收数据
int len = recv(pinfo->cfd, buf, sizeof(buf), 0);
if (len > 0)
{ // 还有数据
printf("Client say : %s\n", buf);
send(pinfo->cfd, buf, sizeof(buf), 0);
}
else if (len == 0)
{ // 说明客户端已经断开了连接
printf("客户端已经断开了连接...!\n");
break;
}
else
{ // len == -1 说明读取数据失败
perror("recv");
break;
}
}
// 6.关闭文件描述符
close(pinfo->cfd);
pinfo->cfd = -1; // 置为 -1 表示这块内存已经是可用的了
return NULL;
}
实现效果:
在分别启动四个客户端。
客户端 1 1 1 :