socket套接字概念
所谓套接字,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进程通信的接口,是应用程序与网络协议进行交互的接口。
它是网络环境中进行通信的API,使用中每一个套接字都有一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入他所在的主机socket中,该socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另一台主机的socket中,使对方能够接收到这段信息。socket是由IP地址和端口结合的,提供应用层进程传送数据包的机制。
socket本意上“插座”的意思,在Linux环境中,用于表示进程间网络通信的特殊文件类型。本质上为内核借助缓冲区形成的伪文件。把它设置为文件,方便我们进行操作,我们可以通过文件描述符进行操作。与管道类型,Linux系统将期封装成文件的目的是为了统一接口,使得读写套接字和读写文件操作一样。区别是管道应用于本地进程间通信,而套接字多用于网络进程间数据的传递。
socket是全双工通信,即在同一时刻既可以数据读入,也可以数据输出。
MAC地址(物理地址)
IP地址(逻辑地址): 在网络中唯一标识一台主机
端口号:在一台主机中唯一标识一个进程
IP+端口号:在网络环境中唯一标识一个进程
套接字原理:(绑定了IP和端口号)
在网络中套接字一定是成对出现的。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
-服务器端:被动接受连接,一般不会主动发起连接
-客户端:主动向服务器发起连接
字节序
现在CPU的累加器一次都能装载(至少)4个字节(32位机),即一个整数。哈哈,我在想32位下指针也是4个字节,设定指针的大小与能够寻址范围有关,累加器的装载量限制了指针的大小和寻址范围。那么这4个字节在内存中排列的顺序将影响它被累加器装载的整数值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
(总而言之:字节序就是存储数据的方式,大家按照统一的规则进行,保证数据传输的正确性)
字节序分为大端字节序(Big-Endian)和小端字节序(Little-Endian)。大端字节序是指一个整数的高位字节存储在内存的低地址位置,低位字节存储在内存的高地址位置。小端字节序则是指一个整数的高位字节存储在内存高地址处,而低位字节则存储在内存的低地址处。
大端:低地址---高位
小端:高地址---低位
记忆方法: 低址对高位为大 高址对低位为小
网络字节序
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然会解释错误。那么怎么解决这个问题呢?假设我们让发送数据的一端总是以大端字节序发送(对就是做一个统一的规定),那么接收数据的一端就知道我接受到的字节序总是大端字节序,如果接受方的字节序为小端,那么只需要大端数据装换成小端字节序就可以了。
TCP/IP协议中规定,网络数据流应采用大端字节序。
为了使用方便和移植性,我们可以调用相应的库函数进行转换...
h - host 主机,主机字节序
to 转换成什么
n - network 网络字节序
s - short unsigned short 端口
l - long unsigned int IP
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
测试下我本机的字节序吧,嘻嘻
#include <stdio.h> #include <arpa/inet.h> //先定义一个联合体 union { int number; char c; }test; //为什么用这个联合体可以测试呢? /* 联合体:所有变量共用一块内存 按最大的成员变量进行申请内存 每一时刻只能有一个成员 对于test: siezof(test) = 4 如果用 c = 1 type ==> 每次都能够取到最低位置 */ int main(void) { test.number = 0x12345678; if(test.c == 0x12) //高位存储在内存的低地址上 { printf("本机为大端字节序\n"); } else { printf("本机为小端字节序\n"); } return 0; }
socket地址
socket地址其实是一个结构体,封装端口和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
之前有说到过,socket套接字上联应用程序,下联协议栈
对一个一个数据包想要在网络中的两台不同主机间的进程(当然我们这里不包括本地套接字),只要确认了对方IP(逻辑地址)和端口就可以将数据传送给对方【MAC地址可以根据ARP协议获取到】
通用socket地址
socket网络编程接口中表示socket地址是结构体sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr{ //已经被废弃掉
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
成员:
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议类型对应。常见的协议族和对应的地址族如下所示:
协议族 | 地址族 | 描述 |
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
协议族 PF_*和地址族AF_*在头文件bits/socket.h中,二者值相同,可以混合使用(反正都是宏定义,宏定义是预处理阶段进行宏替换,所以混着用对编译运行不会有影响)
其实我们很容易看到一个问题,
这个地方使用的是一个固定数 14type:
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度
我们可以看到,14个字节几乎只能装下 IPv4地址。因此,Linux定义了下面这个新的通用的socket地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的【内存对齐可以加快CPU访问速度,内存对齐问题见我的C语言专栏,有详细介绍】
这个结构体定义在:/usr/include/linux/in.h
为了方便理解,我们去掉一些杂乱的信息:
#include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[ 128 - sizeof(__ss_align) ]; }; typedef unsigned short int sa_family_t;
专用socket地址
很多网络编程函数诞生早于IPv4协议(用自定义的协议咯,双方共同约定一个规则),那时候都是使用struck socketaddr结构体,为了向前兼容,现在在socketaddr退化成了 (void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; char sun_path[108]; };
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */ in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; struct in_addr { in_addr_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; /* Transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef uint16_t in_port_t; typedef uint32_t in_addr_t; #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地 址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
IP转换址转换函数
人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPV4地址,以及用十六进制字符串表示IPv6地址,但编程中我们需要先把他们转化为整数(二进制)方能使用。而记录日志相反,我们需要把整数表示的IP地址转化为可读的字符串。
早期:(不推荐使用)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp,struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
这只能处理IPV4的ip地址,不可重入函数
现在:
p:点分十进制的IP字符串
n:表示network,网络字节序的整数
#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
af:AF_INET AF_INE6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
点分十进制 ---> 网络字节序 inet_pton
网络字节序 ---> 点分十进制 inet_ntop
网络套接字函数
socket模型创建流程图(TCP通信流程/CS模型流程图)
头文件:#include <arpa/inet.h>
或者:#include <sys/types/h> #include <sys/socket.h>
int socket(int domain,int type,int protocol);
功能:创建一个套接字
参数:
domain:协议族
AF_INET --> ipv4
AF_INET6 --> ipv6
AF_UNIX AF_LOCAL --> 本地套接字通信(进程间通信)
type:通信过程中使用的协议协议
SOCK_STREAM --> 流式协议
SOCK_DGRAM --> 报式文件
protocol:具体的一个协议,一般写 0
SOCK_STREAM --> 流式文件默认使用 TCP
SOCK_DGRAM --> 报式文件默认使用 UDP
返回值:
成功:返回文件描述符,操作的就是内核缓冲区(socket本质上是一个伪文件)
失败:-1
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
功能:绑定,将fd和本地的IP+端口进行绑定
参数:
sockfd:通过socket函数得到的文件描述符
addr:需要绑定的socket地址,这个地址封装了ip和端口号的信息
addrlen:第二个参数结构体占的内存大小
int listen(int sockfd,int backlog);
功能:监听这个socket上的连接
参数:
sockfd:通过socket()函数得到的文件描述符
backlog:未连接的和、已连接的和的最大值 5
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
参数:
sockfd:用于监听的文件描述符
addr:传出参数,记录连接成功后客户端的地址信息(ip、port)
addrlen:指定第二个参数的对应的内存大小
返回值:
成功:用于通信的文件描述符
失败:-1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
上个程序案例
#include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <stdio.h> //定义IP #define SERVER_IP "127.0.0.1" //定义端口 #define SERVER_PORT 8080 int main(void) { int lfd,cfd; char str[INET_ADDRSTRLEN]; //创建socket套接字 lfd = socket(AF_INET,SOCK_STREAM,0); //TCP ipv4 //绑定 IP(server) 和 端口号(监听) struct sockaddr_in serverAddr; memset(&serverAddr,0,sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVER_PORT); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); //或者 INADDR_ANY:提供任意一个本地有效IP bind(lfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)); //监听 设置最大监听数目 128 listen(lfd,128); //等待连接 struct sockaddr_in clientAddr; socklen_t clientAddr_len = sizeof(clientAddr); cfd = accept(lfd,(struct sockaddr *)&clientAddr,&clientAddr_len); //数据交换 int n,i=0; char buf[1024] = {0}; while(1) { n = read(cfd,buf,sizeof(buf)); if(n == 0) //有客户端断开连接 { printf("有客户端断开连接\n"); } if(n < 0) { printf("aaaaaaaa\n"); } // inet_ntop(AF_INET,&clientAddr.sin_addr,str,sizeof(str)); // ntohs(clientAddr.sin_port); printf("已收到第%d次数据:%s\n",i++,buf); //sleep(2); write(cfd,buf,n); } close(cfd); close(lfd); return 0; }
#include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <stdio.h> //定义IP #define SERVER_IP "127.0.0.1" // //定义端口 #define SERVER_PORT 8080 int main(void) { int sockfd; //创建套接字 TCP ipv4 sockfd = socket(AF_INET,SOCK_STREAM,0); //连接 struct sockaddr_in serverAddr; memset(&serverAddr,0,sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVER_PORT); inet_pton(AF_INET,SERVER_IP,&serverAddr.sin_addr); connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)); //数据交换 char buf[1024] = {0}; int i=0,n=0; while(1) { //memset(buf,0,sizeof(buf)); fgets(buf,sizeof(buf),stdin); //scanf("%s",buf); write(sockfd,buf,sizeof(buf)); memset(buf,0,sizeof(buf)); n = read(sockfd,buf,sizeof(buf)); printf("------a-------\n"); write(STDOUT_FILENO,buf,n); } close(sockfd); return 0; }
客户端与服务器端启动连接后可以使用 netstat -apn|grep 8080查看连接情况
出错处理封装函数
我们知道,系统调用不能保证每次都成功,必须进行错误处理,这样一方面可以保证程序的逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一系列函数加上错误代码封装成新的函数(按照系统的库函数进行封装),做成一个模板 wrap.c
如果有需要,请把这份代码自己保存好,下次可以直接使用.(有兴趣的可以打包成动态库)
先来把头文件搞定:
先来看下我们需要定义哪些东西:其实很简单,其实并不难。先把框架搭起来。欧里给
对呀,就上面这些系统调用,直接粘过来,把每一个都一个错误处理的接口API. 搞定
函数名直接取,最好按驼峰法取名
#ifndef _WRAP_H_ #define _WRAP_H_ void perr_exit(const char *s); int Accept(int fd,struct sockaddr *sa,socklen_t *salenptr); int Bind(int fd,const struct sockaddr *sa,socklen_t salen); int Connect(int fd,const struct sockaddr *sa,socklen_t salen); int Listen(int fd,int backlog); int Socket(int family,int type,int protocol); ssize_t Read(int fd,void *ptr,size_t nbytes); ssize_t Write(int fd,const void *ptr,size_t nbytes); int Close(int fd); ssize_t Readn(int fd,void *vptr,size_t n); ssize_t Writen(int fd,const void *vptr,size_t n); ssize_t my_read(int fd,char *ptr); ssize_t Readline(int fd,void *vptr,size_t maxlen); #endif
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <error.h> void perr_exit(const char *s) { perror(s); exit(1); } int Accept(int fd,struct sockaddr *sa,socklen_t *salenptr) { int n; //accept:阻塞,是慢系统调用。可能会被信息中断 again: if((n = accept(fd,sa,salenptr)) < 0) { if((errno == ECONNABORTED) || (errno == EINTR)) { goto again; //重启 } else { perr_exit("accept error"); } } return n; } int Bind(int fd,const struct sockaddr *sa,socklen_t salen) { int n; if((n = bind(fd,sa,salen)) < 0) { perr_exit("bind error"); } return n; } int Connect(int fd,const struct sockaddr *sa,socklen_t salen) { int n; if((n = connect(fd,sa,salen)) < 0) { perr_exit("connect error"); } return n; } int Listen(int fd,int backlog) { int n; if((n = listen(fd,backlog)) < 0) { perr_exit("listen error"); } return n; } int Socket(int family,int type,int protocol) { int n; if((n = socket(family,type,protocol)) < 0) { perr_exit("socket error"); } return n; } ssize_t Read(int fd,void *ptr,size_t nbytes) { ssize_t n; again: if((n = read(fd,ptr,nbytes)) == -1) { if(errno == EINTR)//被中断 { goto again; } else { return -1; } } return n; } ssize_t Write(int fd,const void *ptr,size_t nbytes) { ssize_t n; again: if((n = write(fd,ptr,nbytes)) == -1) { if(errno == EINTR) { goto again; } else { return -1; } } return n; } int Close(int fd) { int n; if((n = close(fd)) == -1) { perr_exit("close error"); } return n; } ssize_t Readn(int fd,void *vptr,size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while(nleft > 0) { if((nleft = read(fd,ptr,nleft)) < 0) { if(errno == EINTR) { nread = 0; } else { return -1; } } else if(nread == 0) { break; } nleft -= nread; ptr += nread; } return n-nleft; } ssize_t Writen(int fd,const void *vptr,size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while(nleft > 0) { if((nwritten = write(fd,ptr,nleft)) <= 0) { if(nwritten < 0 && errno == EINTR) { nwritten = 0; } else { return -1; } } nleft -= nwritten; ptr += nwritten; } return n; } static ssize_t my_read(int fd,char *ptr) { static int read_cnt; static char *read_ptr; static char read_buf[100]; if(read_cnt <= 0) { again: if((read_cnt = read(fd,read_buf,sizeof(read_buf))) < 0) { if(errno == EINTR) { goto again; } return -1; } else if(read_cnt == 0) { return 0; } read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return 1; } ssize_t Readline(int fd,void *vptr,size_t maxlen) { ssize_t n,rc; char c,*ptr; ptr = vptr; for(n=1;n<maxlen;n++) { if((rc = my_read(fd,&c)) == 1) { *ptr++ = c; if(c == '\n') { break; } } else if(rc == 0) { *ptr = 0; return n-1; } else { return -1; } } *ptr = 0; return n; }