在线支付网站建设,交互式网站,网站建设三把火科技,wordpress段代码一、什么是socket Socket套接字 由远景研究规划局#xff08;Advanced Research Projects Agency, ARPA#xff09;资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将 TCP/IP 协议相关软件移植到UNIX类系统中。设计者开发了一个接口#xff0c;以便应用程序能简单地…一、什么是socket Socket套接字 由远景研究规划局Advanced Research Projects Agency, ARPA资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将 TCP/IP 协议相关软件移植到UNIX类系统中。设计者开发了一个接口以便应用程序能简单地调用该接口通信。这个接口不断完善最终形成了 Socket套接字。Linux系统采用了Socket套接字因此Socket接口就被广泛使用到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h 中。 socket 其实就是一个网络通信的接口或者说是标准我们通过使用这个接口就能进行网络通信。在进行通信之前我们需要确定对方主机的地址也就是对方主机的 IP 地址由于网络通信的消息是发送给对方主机上的某一个具体的进程的比如用 QQ 发送消息给对方对方也是用 QQ 这个应用接收消息的所以我们需要指定将这个消息发送给对方主机上 QQ 这个应用我们通过指定 QQ 这个进程对应的 Port端口号 完成。
注意socket 翻译过来的意思是 插座有插座的话那么就存在 插头插头 看作 客户端插座 看成 服务端将 插头 插到 插座 上的这个过程就类似于 客户端和服务端通信建立连接的过程。肯定是先要有 插座才会有插头实际应用中应该是先启动客户端等待客户端的请求连接再启动客户端建立连接。
二、socket
1.字节序 字节顺序又称端序或尾序英语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);2. IP 地址
IP地址实际本质上是一个整数但是在形式上我们用一个 “点分十进制” 的字符串来描述比如本地地址是 127.0.0.1。
我们可以通过下面的函数将 主机字节序(小端序)的 IP地址(字符串) 转换为 网络字节序(大端序整数)
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 参数
afIP地址族包括AF_INET IPv4格式的 IP 地址AF_INET6 IPv6格式的 IP 地址srcIP 地址一个点分十进制的字符串比如 127.0.0.1dst传出参数对应的 src 被转换成 网络字节序(大端序) 之后就将结果写到了 *dst 这个指针所指的内存处
返回值
返回 1 1 1表示转换成功返回 0 o r − 1 0\ or\ -1 0 or −1表示失败
通过下面这个函数可以将 网络字节序(大端序)的 IP 地址(整数) 转换成 主机字节序(小端序)点分十进制的 IP 地址(字符串)
#include arpa/inet.h
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);参数
afIP地址族包括AF_INET IPv4格式的 IP 地址AF_INET6 IPv6格式的 IP 地址src网络字节序的整型(大端序) IP 地址dst传出参数将 src 转换成的 主机字节序(小端序)的 点分十进制的 IP 地址size修饰 dst表示 dst 指向的内存最多可以存入 size 个字节的数据
返回值
成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的 IP字符串失败返回 NULL
3.sockaddr 结构 // 在写数据的时候不好用
struct sockaddr {sa_family_t sa_family; // 地址族协议, ipv4char 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)];
}; 4.套接字函数
使用套接字通信函数需要引入头文件 arpa/inet.h
1.创建套接字
// 创建一个套接字
int socket(int domain, int type, int protocol);参数
domain使用的地址族协议。AF_INET使用 IPv4 格式的 IP地址AF_INET6使用 IPv6 格式的 IP 地址typeSOCK_STREAM使用流式传输的协议TCP协议SOCK_DGRAM使用报文传输的协议UDP协议protocal一般写 0 0 0使用默认协议。SOCK_STREAM 默认使用的是 TCP 协议SOCK_DGRAM 默认使用的是 UDP 协议
返回值
成功返回可用于套接字通信的 文件描述符失败返回 − 1 -1 −1
该函数执行成功的返回值是一个 文件描述符通过这个文件描述符可以操作内核中的某一块内存网络通信就是基于这个文件描述符来完成的。
2.绑定
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数
sockfd用于监听的文件描述符是通过调用 socket() 函数的返回值addr要绑定的 IP 地址 和 端口信息 需要在这个结构体中初始化IP 地址 和 端口信息需要转换为 网络字节序(大端序)addrlen参数 addr 指向内存的大小即 sizeof (struct addr)
返回值
成功返回 0 0 0失败返回 − 1 -1 −1
3.监听
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);参数
sockfd文件描述符通过调用 socket()函数的返回值在监听之前必须先绑定 bind()backlog能够同时处理的最大连接数最大值为 128 128 128
返回值
成功返回 0 0 0失败返回 − 1 -1 −1
4.获取连接
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数
sockfd监听的文件描述符addr建立连接的客户端的信息addrlen用于存储 addr 指向内存的大小
返回值
成功返回一个文件描述符用于和建立连接的这个客户端进行通信失败返回 − 1 -1 −1
注意 这个函数是一个阻塞函数当没有新的客户端连接请求的时候该函数阻塞在这里当检测到有新的客户端连接请求时阻塞解除新连接就建立了得到的返回值也是一个文件描述符基于这个文件描述符就可以和客户端通信了。
5.接收数据
// 发送数据的函数
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 0 0实际接收数据的字节数等于 0 0 0对方断开了连接 − 1 -1 −1接收数据失败了
注意 如果连接没有断开接收端接收不到数据接收数据的函数会阻塞等待数据到达数据到达后函数解除阻塞开始接收数据当发送端断开连接接收端无法接收到任何数据但是这时候就不会阻塞了函数直接返回0。
6.发送数据
// 发送数据的函数
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 0 0实际发送的字节数和参数 len 是相等的 − 1 -1 −1发送数据失败
7.连接
// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数:
sockfd: 通信的文件描述符, 通过调用 socket() 函数就得到addr: 存储了要连接的服务器端的地址信息: IP 和 端口这个 IP 和端口也需要转换为网络字节序大端序然后再赋值addrlen: addr 指针指向的内存的大小 sizeof(struct sockaddr)
返回值
连接成功返回 0 0 0连接失败返回 − 1 -1 −1
三、TCP 通信
TCP 是一个面向连接的、安全的、流式传输协议是传输层的协议。
TCP 通信的大致流程如下 服务端的通信流程
1.创建用于监听的套接字 lfd
int lfd socket();2.将监听得到的文件描述符 lfd 和本地端口 和 IP 地址绑定 bind();3.设置监听监听客户端的连接 listen();4.等待并接受客户端的连接请求建立新的连接会返回一个文件描述符 cfd用于通信的没有客户端连接就一直阻塞 int cfd accept();5.通信进行收发数据读写操作默认都是阻塞的 // 接收数据
read(); / recv();
// 发送数据
write(); / send();6.断开连接关闭套接字 close();在服务端有两种文件描述符 用于 监听 的文件描述符 只需要有一个即可 只负责检测客户端的连接请求检测到之后调用 accept() 就能建立新的连接 用于 通信 的文件描述符 负责和建立连接的客户端进行通信 如果有 k k k 个客户端和服务端建立了连接那么用于通信的文件描述符就有 k k k 个每一个客户端都与服务端有一个对应的用于通信的文件描述符
文件描述符对应的内存结构
一个文件描述符对应两块内存分别是 读缓冲区 和 写缓冲区读数据 通过 文件描述符 将内存中的数据读出这块内存就是读缓冲区写数据 通过 文件描述符 将数据写入某块内存中这块内存就是写缓冲区
用于监听的文件描述符
客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中读缓冲区中有数据, 说明有新的客户端连接调用 accept() 函数, 这个函数会检测监听文件描述符的读缓冲区 检测不到数据, 该函数阻塞 如果检测到数据, 解除阻塞, 新的连接建立
用于通信的文件描述符 客户端和服务端都有用于通信的文件描述符 发送数据调用 write() / send() 函数将数据写到内核 数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中 内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中 接收数据调用 read() / recv() 函数, 从内核读数据 数据进入到通信的文件描述符的读缓冲区中 数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可
客户端的通信流程 在单线程的情况下客户端用于通信的文件描述符只有一个没有用于监听的文件描述符。 1.创建一个用于通信的套接字
int cfd socket();2.连接服务端这时需要知道服务端绑定的 IP 地址 和 端口号
connect();3.开始通信进行收发数据
// 接收数据
read(); / recv();
// 发送数据
write(); / send();4.断开连接关闭文件描述符
close();四、实现代码
1.最初版
此时只能是一个客户端 跟 一个服务端进行通信。
服务端代码 server.c
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include arpa/inet.hint 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; //IPv4saddr.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 stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include arpa/inet.hint 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; //IPv4saddr.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;
}实现效果 最后是客户端先断开连接 服务端也断开连接 2.多线程版
对于每一个客户端的连接服务端都生成一个子线程去跟客户端进行通信。这样就可以多个客户端连接同一个服务端。
我们需要将服务端的代码改成多线程的版本。
server_mutl_thread.c
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include arpa/inet.h
#include pthread.h//信息结构体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; // IPv4saddr.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 客户端 2 2 2 客户端 3 3 3 客户端 4 4 4 五、参考内容
本篇博客是对于 套接字 Socket 的整理。