进程间通信:
特点:依赖于内核,造成缺陷——无法实现多机通信。
网络:
地址:由IP地址(IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。)和端口号构成(所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号 ,端口包括逻辑端口和物理端口两种类型),实际上是通过IP地址+端口号来区分不同服务的,端口号提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如:对于每个TCP/IP实现来说,FTP服务器的TCP端口号就是21,每个Talnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69.
数据交流:
当涉及到数据交流的时候就会涉及到协议(说白了就是数据格式),例如:http(超文本传输协议是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。)、tcp(传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。)、udp(Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768描述了 UDP。)
socket套接字:
- 所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
- 所谓socket网络编程,也叫作套接字网络编程,套接字网络编程用到的协议TCP协议和UDP协议的用的比较多。
- 连接方面区别:TCP提供面向连接的传输,通信前要先建立连接(三次握手机制)(如打电话要先拨号建立连接)。UDP是无连接的,面向报文的,即发送数据之前不需要建立连接。
- 安全方面的区别:TCP提供可靠的服务,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达。UDP尽最大努力交付,即不保证可靠交付。
- 传输效率的区别:TCP传输效率相对较低。UDP传输效率高,适用于对高速传输和实时性有较高的通信或广播通信。
- 连接对象数量的区别:TCP连接只能是点到点、一对一的。UDP支持一对一,一对多,多对一和多对多的交互通信。
- TCP首部开销20字节,UDP的首部开销小,只有8个字节
字节序:
字节序,即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。
为什么会有小端字节序?
- 计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
- 但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
常见序:
- Little endian(小端字节序):将低序字节存储在起始地址
- Big endian(大端字节序):将高序字节存储在起始地址
- 网络协议指定了通讯字节序—大端
- 只有在多字节数据作为整体处理时才需要考虑字节序
- 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
- 异构计算机之间通讯,需要转换自己的字节序为网络字节序
什么是高低位:
- 给一个十进制整数,123456,很明显左边的是高位,右边的是低位。计算机也是这样认为的。给一个16进制数(四个二进制表示),0x12345678,以字节为单位,从高位到低位依次是 0x12、0x34、0x56、0x78。
- 例子:在内存中双字0x01020304(DWORD)的存储方式
地址从低到高:4000->4001->4002->4003
小端字节序: 04 03 02 01
大端字节序:01 02 03 04
socket编程步骤:
可以把服务器与客户端之间的场景看做以下场景:一个客户端走到五座房子(5个服务器)的面前,要访问这五座房子中的一座房子中的一间房间。当客户端不知道去哪一间房子时,这时候有一个人在楼上喊我是说汉语的(TCP/UDP)服务器,我的楼号是…(IP地址),我的房间号是…(端口号),然后客户端就可以获取服务器IP和服务器端口号进行连接。
服务器端创建步骤:
- 服务器端创建套接字(socket函数),返回网络描述符,后续用网络描述符进行操作
- bind()为套接字添加信息,指定服务器自己的IP地址和端口号
- 监听网络连接(listen()函数)
- 监听到有客户端接入,接收一个连接(accept()函数)
- 数据交互(利用read函数从网络上面读数据,利用write函数向网络上面写数据)
- 关闭套接字(close()函数),断开连接
客户端创建步骤:
- socket()创建一个通道
- connect()连接服务器,根据IP地址和端口号
- 然后进行读写操作
- 最后关闭套接字断开连接
三次握手和一次挥手:
当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。
参数:
domain:函数socket()的参数domain用于设置网络通信的域
函数socket()根据这个参数选择通信协议的族,通常为AF_INET,
表示互联网协议族(TCP/IP协议族)。通信协议族在文件sys/socket.h中定义。
协议族决定了socket的地址类型,在通信中必须采用对应的地址
domain的值及含义:
AF_INET、PF_INET: IPV4因特网域
AF_INET6: IPV6因特网域
AF_UNIX: Unix域
AF_ROUTE: 路由套接字
AF_KEY: 密钥套接字
AF_UNSPEC: 未指定
PF_UNIX,PF_LOCAL: 本地通信
type:指定socket类型
常用的socket类型有:
SOCK_STREAM: Tcp连接,提供序列化的、可靠的、双向连接的字节流,使用TCP协议。支持带外数据传输
SOCK_DGRAM: 数据报套接字定义了一种无连接的报,数据通过相互独立的报文件进行传输,
是无序的,并且不保证是可靠的、无差错的、使用数据报协议UDP连接(无连接状态的消息)
SOCK_RAW: RAW类型,允许程序使用底层协议,原始套接字允许对底层协议如IP/ICMP进行直接访问,
功能强大但使用较为不便,主要用于协议的开发
SOCK_PACKET: 这是一个专用类型,不能呢过在通用程序中使用
SOCK_SEQPACKET:序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,
数据长度定常。每次调用读系统调用时数据需要将全部数据读出
protocol:故名思意,就是指定协议.
常用的协议有:
IPPROTO_TCP:TCP传输协议
IPPTOTO_UDP:UDP传输协议
IPPROTO_SCTP:STCP传输协议
IPPROTO_TIPC:TIPC传输协议
返回值:
如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
- 注意: 并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
- 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
bind()函数:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。
例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
参数:
sockfd:即socket描述字,它是通过socket()函数创建了唯一标识一个socket。
bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址
这个地址结构根据地址创建socket时的地址协议族的不同而不同。
如ipv4对应的是:
struct sockaddr{
unsigned short as_familf;//协议族
char sa_data[14];//IP+端口号
}
同等替换为:
使用时,强转:
(struct sockaddr *)&my_addr
struct sockaddr_in {
sa_family_t sin_family; /* 协议族: AF_INET */
in_port_t sin_port; /* 端口号,一般用5000以上,低于3000的是操作系统的关键端口,
这个要将它转化为网络字节序*/
struct in_addr sin_addr; /* IP地址结构体*/
unsigned char sin_zero[8]/*填充没有实际意义只是为跟sockaddr结构体在内存中对齐,这样两者才能相互转换*/
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
addrlen:对应的是地址的长度,结构体大小。
返回值:
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败
- 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
- 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
IP地址转换API:
旧版本:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
功能:
将点分十进制IP转化为网络字节序存放在addr中,并返回该网络字节序对应的整数。
参数:
输入参数cp包含ASCII表示的IP地址。
输出参数inp是将要用新的IP地址更新的结构。
返回值:
如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。
使用这个函数并没有错误码存放在errno中,所以他的值会被忽略。
in_addr_t inet_addr(const char *cp);
功能:
将点分十进制IP转化为网络字节序(二进制位的大端存储)。
返回值;
如果失败:返回INADDR_NONE;
如果成功:返回IP对应的网络字节序的数;
char *inet_ntoa(struct in_addr in);
功能:
把网络格式的ip地址转化为字符串形式
inet_network函数
in_addr_t inet_network(const char *StrIP)
功能:
将点分十进制IP转化为主机字节序(二进制位小端存储)。
返回值:
如果失败:返回-1;
如果成功:返回主机字节序对应的数;
旧版本的只能处理IPv4的ip地址
不可重入函数
注意参数是struct in_addr
新版本:
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
新版本的支持IPv4和IPv6
可重入函数
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,
还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
listen()、connect()函数:
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
内核为任何一个给定监听者套接字,维护两个队列:
1、未完成队列,每个这样的SYN报文段对应其中一项
已由某个客户端发出并到达服务器,而服务器正在
等待相应的TCP三次握手过程,这些套接字处于SYN_REVD状态
2、已完成队列,每个已完成TCP三次握手过程的客户端
对应其中一项,这些套接字处于ESTABLISHED状态。
参数:
listen函数的第一个参数即为要监听的socket描述字服务器的描述字
第二个参数为相应socket可以排队的最大连接个数。
listen()并未开始连线,只是设置socket的listen模式。
listen函数只用于服务器端。
socket()函数创建的socket默认是一个主动类型的,
listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,
第二参数为服务器的socket地址,第三个参数为socket地址的长度。
客户端通过调用connect函数来建立与TCP服务器的连接。
返回值:
listen函数:成功返回0, 失败返回-1.
connect函数:成功返回0, 失败返回-1.
accept()函数:
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
accept函数的第一个参数为服务器的socket描述字,
第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,不关心可以设置为NULl。
第三个参数为协议地址的长度。
返回值:
如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
失败时返回-1
- 注意: accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
数据的收发:
一般用read/write函数,当然还有其他函数:
- read/write
- recv()/send()
- readv()/writev()
- recvmsg()/sendmsg()(多在UDP连接时使用)
- recvfrom()/sendto()(多在UDP连接时使用)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()/send()函数:
close()函数:
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h>
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭
然后立即返回到调用进程。该描述字不能再由调用进程使用
也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1
只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
网络字节序和主机字节序的转换:
#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位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
服务端实现可被连接功能:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
//#include<linux/in.h> 头文件之间可能造成冲突这个头文件就和#include <arpa/inet.h>冲突
#include <arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include <sys/wait.h>
#include<pthread.h>
struct num
{
int fd;
char*write;
};
void* writeMsg(void*arg)
{
int i;
struct num Part;
Part.write=((struct num*)arg)->write;
Part.fd=((struct num*)arg)->fd;
while(1){
if(i==1){
getchar();
}
i=1;
memset(Part.write,'\0',128);
printf("输入要给客户端的内容:\n");
scanf("%[^\n]",Part.write);
write(Part.fd,Part.write,128);
}
}
int main()
{
struct sockaddr_in IP;
struct sockaddr_in CLI;//客户端信息
struct num CAN;
int socketre;
int bindre;
int i=0;
int newfd;
pid_t fpid;
pthread_t sontd;
char* writebuf;
char* readbuf;
readbuf=(char*)malloc(128);
writebuf=(char*)malloc(128);
int len=sizeof(struct sockaddr_in);
socketre=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(socketre==-1){
printf("create fail\n");
perror("socket");
exit(-1);
}
memset(&IP,'\0',len);
memset(&CLI,'\0',len);
IP.sin_family=AF_INET;
IP.sin_port=htons(8686);
IP.sin_addr.s_addr=inet_addr("192.168.1.183");
bindre=bind(socketre,(struct sockaddr*)&IP,len);
if(bindre==-1){
perror("bind");
printf("bind fail\n");
exit(-1);
}
listen(socketre,10);
while(1){
newfd=accept(socketre,(struct sockaddr*)&CLI,&len);
if(newfd==-1){
perror("accept");
printf("accept fail\n");
exit(-1);
}
printf("连接成功\n");
printf("get client:%s\n",inet_ntoa(CLI.sin_addr));
CAN.write=writebuf;
CAN.fd=newfd;
fpid=fork();
if(fpid==0){
pthread_create(&sontd,NULL,writeMsg,(void*)&CAN);
while(1){
memset(readbuf,'\0',128);
if(read(newfd,readbuf,128)!=0){
if(strcmp(readbuf,"quit")==0){
write(newfd,"server is out",13);
close(newfd);
pthread_cancel(sontd);
break;
}
printf("接受到消息:%s\n",readbuf);
printf("输入要给客户端的内容:\n");
}
else{
printf("与客户端断开连接\n");
}
}
}
if(fpid>0){
waitpid(fpid,NULL,WNOHANG | WUNTRACED);
}
}
return 0;
}
如果有手机的话可以下载TCP手机助手,进行连接服务器,没有该软件可以用以下客户端代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<pthread.h>
struct Client
{
char* write;
int fd;
};
void* del(void*arg)
{
struct Client CAN;
CAN.write=((struct Client*)arg)->write;
CAN.fd=((struct Client*)arg)->fd;
while(1){
memset(CAN.write,'\0',128);
printf("请输入要发送给服务端的消息:\n");
scanf("%s",CAN.write);
write(CAN.fd,CAN.write,strlen(CAN.write));
}
}
int main()
{
int socketfd;
int conre;
char*writebuf;
char*readbuf;
pthread_t th;
struct Client CL;
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8686);
addr.sin_addr.s_addr=inet_addr("192.168.1.183");
socketfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(socketfd==-1){
printf("socket create fail\n");
perror("socket");
exit(-1);
}
conre=connect(socketfd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in));
if(conre==-1){
printf("connect fail\n");
perror("connect");
exit(-1);
}
writebuf=(char*)malloc(128);
readbuf=(char*)malloc(128);
CL.write=writebuf;
CL.fd=socketfd;
pthread_create(&th,NULL,del,(void*)&CL);
while(1){
memset(readbuf,'\0',128);
read(socketfd,readbuf,128);
printf("获取到服务端数据:%s\n",readbuf);
printf("请输入要发送给服务端的消息:\n");
}
return 0;
}
补充:
- 当我们想用一个结构体或者联合体时,可以进入/usr/include/这个文件夹下面,查找看看头文件里面有没有包含想要使用的结构体或者联合体,使用以下命令:
cd /usr/include/
进入文件夹
grep "struct sockaddr_in {" * -nir
grep表示查找,双引号内的东西是你要查找的内容的一部分
*表示在当前目录下。-nir中 n表示显示行号,i表示不区分大小写,r表示逐行扫描
linux/in.h:232:struct sockaddr_in {
得到结果:232表示行号,linux/in.h表示所在文件夹
vi linux/in.h +232
这个命令是直接打开定位到该文件的232行。
- linux查看端口号占用命令-netstat
netstat -pan | grep 5623
#其中5623位端口号
如图:可以看到占用该端口号的进程,并且可以利用ps指令找到程序名称。
本文参照博客:socket编程