1. 套接字的概念
Socket ,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
区别与管道主要应用于本地进程间通信,套接字多应用于网络进程间数据的传递!!!
在TCP/IP协议中,IP地址(32位)可在网络环境中,唯一标识一台主机;端口号(16位, 上限2^16 = 65536)可在一台主机上,唯一标识一个进程。所以,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程!!!!
“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系
在网络通信中,套接字一定是成对出现的。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
2. 网络字节序
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
一般主机采用小端存储,所以两者通信要做字节序的转换
大端:低地址存放高字节数(网络存储)
小端:低地址存放低字节数(PC本地存储)
2.1 网络字节序和主机字节序的库函数转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 本地->网络(32位, IP地址)
uint16_t htons(uint16_t hostshort); // 本都->网络(16位, 端口号)
uint32_t ntohl(uint32_t netlong); // 网络->端口(32位, IP地址)
uint16_t ntohs(uint16_t netshort); // 网络->端口(16位, 端口号)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
注意:日常使用IP地址(128.168.188.120)是点分十进制表示,本质是 string 字符串,需要调用 atoi 函数转为 int 二进制后,才能调用 htonl 进行字节序转换
2.2 IP地址转换函数
点分十进制转二进制,本地字节序(string IP) -> 网络字节序
man inet_pton // convert IPv4 and IPv6 addresses from text to binary form
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数说明:
af:AF_INET、AF_INET6(分别代表IPV4、IPV6)
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址
返回值:
成功: 1
异常: 0, 说明 src 指向的不是一个有效的 ip 地址
失败:-1
二进制转点分十进制,网络字节序 -> 本地字节序(string IP)
man inet_ntop // convert IPv4 and IPv6 addresses from binary to text form
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明:
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值:
成功:dst
失败:NULL
2.3 sockaddr 数据结构
IP + port : 可在网络环境中唯一标识一个进程
man 7 ip
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 {
uint32_t s_addr; /* address in network byte order */
};
初始化结构体的demo:
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
// addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
因历史发展原因,为了向前兼容,在不同函数调用中需做参数类型的强制转换
3. 网络套接字函数
3.1 socket模型创建流程图
3.2 socket 函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol); // 创建一个 套接字
参数说明:
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功:新套接字所对应文件描述符
失败:-1,errno
3.3 bind 函数
给socket绑定一个 地址结构 (IP+port)
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd: socket 函数返回值
addr: 传入参数(struct sockaddr *)&addr
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addrlen: sizeof(addr) 地址结构的大小
返回值:
成功:0
失败:-1 errno
3.4 listen 函数
设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明:
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
3.5 accept 函数
阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
addrlen:传入传出。 &clit_addr_len
socklen_t clit_addr_len = sizeof(addr);
入:addr的大小。 出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
3.6 connect 函数
使用现有的 socket 与服务器建立连接
initiate a connection on a socket
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd: socket 函数返回值
addr:传入参数。服务器的地址结构
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
4. socket API demo
4.1 TCP通信流程分析:
server:
-
socket() 创建socket
-
bind() 绑定服务器地址结构
-
listen() 设置监听上限
-
accept() 阻塞监听客户端连接
-
read(fd) 读socket获取客户端数据
-
小–大写 toupper()
-
write(fd)
-
close();
client:
-
socket() 创建socket
-
connect(); 与服务器建立连接
-
write() 写数据到 socket
-
read() 读转换后的数据。
-
显示读取结果
-
close()
4.2 server demo 服务器端
/* server.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<ctype.h>
#include<sys//socket.h>
#include<arpa/inet.h>
#define SERV_PORT 9537
void sys_err(const char* str)
{
perror(str);
exit(1);
}
int main(int argc, char * argv [ ])
{
int lfd, cfd ;
int ret, i;
char buf[BUFSIZ], client_IP[1024];
struct sockaddr_in serv_addr, clit_addr;
socklen_t clit_addr_len;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(lfd, 128);
clit_addr_len = sizeof(clit_addr);
cfd = accept(lfd, (struct sockaddr*)&clit_addr, &clit_addr_len);
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
ntohs(clit_addr.sin_port));
while(1)
{
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
for (i = 0; i < ret; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, ret);
}
close(lfd);
close(cfd);
return 0;
}
注意: 实际工程中,切记一定检查函数返回值
nc 192.168.188.100 9537 // 与服务器进行通信
client demo 客户端
/* client.c */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SERV_PORT 9537
int main(int argc, char * argv [])
{
int cfd, ret;
int conter = 10;
char buf[BUFSIZ];
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
cfd = socket(AF_INET, SOCK_STREAM, 0);
connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
while(--conter)
{
write(cfd, "hello\n", 6);
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO,buf, ret);
sleep(1);
}
return 0;
}