Socket套接字
不同主机间的相互通信:❤️
-
进 程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施:
- UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
- UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
-
上述的进程间通信的方式都是依靠Liunx内核进行的,所以只能用在本机进程之间通信。网间进程通信就解决了不同主机进程间的相互通信的问题
-
IP地址:在Internet上为每台计算机指定的唯一的32位地址称为”IP地址“,也称”网际结构“。IP地址具有固定、规范的格式,它由32位二进制数组成,分成4段,其中每8位构成一段,这样,每段所能表示的二进制数的范围最大不超过255,段与段之间用”."隔开。为了便于标识和表达,IP地址以十进制形式表示,每8位二进制数用一个十进制数表示。
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char* straddr,struct in_addr *addrp);//点分十进制转二进制,第一个参数为十进制IP,第二个参数指向二进制的存放 char* inet_ntoa(struct in_ addr inaddr);//二进制转点分十进制
-
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
-
TCP/IP协议存在于OS中,网络服务通过OS提供,在OS中增加支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等
-
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种:
- TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
套接字socket:❤️
- socket即是一种特殊的文件(S),可以用“打开open –> 读写write/read –> 关闭close”模式来操作
- socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
- 其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
网络字节序与主机字节序:❤️
-
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序
-
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
- Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
上图来自网络
-
网络字节序:4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
-
字节序转换函数
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); //h表示host,主机,n表示network,网络,l表示32位长整数(long),s表示16位短整数(short)。
Socket编程:❤️
Socket编程的一般步骤
服务端:
-
创建Socket套接字,用Socket ()函数
-
设置Socket属性
-
为套接字添加信息(IP地址和端口号)用bind()函数
-
开启监听,用listen()函数
-
监听到有客户端接入,接受一个连接,用accept()函数
-
交换数据,用函数send()和recv(),或者用read(),write()
-
关闭套接字,断开连接
客户端:
- 创建一个Socket,用Socket函数
- 设置Socket属性
- 连接服务器,用connect()函数
- 交换数据,用函数send()和recv(),或者用read(),write()
- 关闭网络连接
socket接口函数
头文件
#include <sys/types.h>
#include <sys/socket.h>
Socket函数
-
函数原型
int socket(int domain, int type, int protocol);
-
功能:用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数
-
参数:
- protofamily:协议族(family)。常用的协议族有:AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
- protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
-
返回值: 成功时返回非负整数(socket描述符),失败返回 -1。
-
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
bind函数
-
函数原型
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
-
功能:绑定IP地址和端口号到Sockfd
-
sockfd: 一个socket描述符,由socket函数调用返回
-
addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要邦定给sockfd的协议地址结构,这个地址结构根据地址创建,socket时的地址协议族的不同而不同。
- ipv4用的结构体:
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 */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
-
addrlen: 第二个参数中结构体的大小
-
返回值:成功返回0,出错返回-1
listen和connect函数
-
函数原型
int listen(int sockfd, int backlog);//把进程变为一个服务器,并指定相应的套接字变为被动连接。规定内核为相应套接字排队的最大连接数 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//绑定之后client端(客户端),与服务器建立连接参数
-
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
-
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
-
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
-
返回值:成功返回0,出错返回-1
accept函数
-
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
-
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
-
参数:
-
sockfd:监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。
-
addr:这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
-
len:addr结构的大小的,它指明addr结构所占有的字节个数。可以被设置为NULL。
-
-
返回值:accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。失败返回-1。
-
如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。
代码示例:❤️
-
客户端client.c:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main(int argc,char**argv) { int n_read; char readBuf[128] = {0}; char data[64]={0}; int c_fd = socket(AF_INET,SOCK_STREAM,0); if(c_fd == -1){ perror("socket error"); exit(-1); } struct sockaddr_in c_addr; c_addr.sin_family = AF_INET; c_addr.sin_port = htons(atoi(argv[2])); inet_aton(argv[1],&c_addr.sin_addr); if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(c_addr)) == -1){ perror("connect error"); exit(-1); } if(fork() == 0){ while(1){ printf("put:"); memset(data, 0, strlen(data)); gets(data); write(c_fd,data,strlen(data)); n_read = read(c_fd,readBuf,128); if(n_read == -1){ perror("read error"); exit(-1); }else{ printf("get server data %dbyte:%s\n",n_read,readBuf); } } } wait(NULL); return 0; }
-
服务端server.c:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main(int argc,char**argv) { int n_read; char readBuf[128] = {0}; int c_fd; int s_fd = socket(AF_INET,SOCK_STREAM,0); if(s_fd == -1){ perror("socket error"); exit(-1); } struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(atoi(argv[2])); inet_aton("argv[1]",&s_addr.sin_addr); bind(s_fd,(struct sockaddr *)&s_addr,sizeof(s_addr)); listen(s_fd,10); struct sockaddr_in c_addr; int c_size = sizeof(c_addr); while(1){ c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&c_size); if(c_fd == -1){ perror("accept error"); exit(-1); } printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr)); if(fork() == 0){ while(1){ memset(readBuf, 0, strlen(readBuf)); n_read = read(c_fd,readBuf,128); if(n_read == -1){ perror("read error"); exit(-1); }else{ printf("get data %dbyte:%s\n",n_read,readBuf); write(c_fd,"I get it!",strlen("I get it!")); } } } } wait(NULL); return 0; }
-
终端输入(第二个参数为IP地址,127.0.0.1是本地环回;第三个参数为端口号,没有被占用的端口可以随便写):
./client.out 127.0.0.1 6666 ./server.out 127.0.0.1 6666
-
客户端:
-
服务端: