什么是socket:
socket的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一-种约定或一-种方式。 通过socket这种约定,- -台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而socket就是用来连接到因特网的工具。
Unix/Linux中单socket是什么:
在UNIX/Linux系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。为了表示和区分已经打开的文件,UNIX/Linux会给每个文件分配一个ID, 这个ID就是一个整数,被称为文件描述符(File Descriptor) 。例如:通常用0来表示标准输入文件(stdin) ,它对应的硬件设备就是键盘;通常用1来表示标准输出文件(stdout),它对应的硬件设备就是显示器。UNIX/Linux程序在执行任何形式的I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、 管道、终端、键盘、显示器,甚至是一个网络连接。请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。我们可以通过socket() 函数来创建-一个网络连接, 或者说打开-一个网络文件,socket()的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:用read() 读取从远程计算机传来的数据;用write() 向远程计算机写入数据。你看,只要用socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!在TCP/IP协议中,“ IP地址+TCP或UDP端口号”唯-标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有-一个socket来标识,那么这两个socket组成的socket pair就唯一标识- 一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
在网络通信中,套接字- - 定是成对出现的。- 端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。 本章的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX DomainSocket 的函数接口。
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。 但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
h表示host,n表示network,1表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回。
sockaddr数据结构
struct sockaddr很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
网络套接字函数
socket模型创建流程图
socket函数
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流动的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
bind函数
服务器程序所监听的网络地址和端口号通常是不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。Struct sockaddr* 是一个通用指针类型,addr参数实际上可以接收多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体长度如:
首先将整个结构体清零,然后设置地址类型为AF_INET, 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
listen函数
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态, listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败就返回-1。
accept函数
三次握手完成后,服务器调用accept()接收连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),出入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传入的是客户端地址结构体的实际长度(有可能没占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端地址。
我们的服务器程序结构是这样的:
整个是一个while死循环,每次循环处理一个客户端连接。由于chiaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listnefd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,失败返回-1。
connect函数
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别再于bind 的参数是自己的地址,而connect的参数的是对方的地址。Connect()成功返回0,出错返回-1。
C/S模型-TCP:(在三次握手四次挥手中还会有详细讲解)
服务器调用socket()、bind()、 listen() 完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket ()初始化后,调用connect ()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段, 客户端收到后从connect()返回,同时应答- -一个ACK段,服务器收到后从accept()返回。
数据传输的过程:建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的
流程是由客户端主动发起请求,服务器被动处理请求,一问- -答的方式。因此,服务器从accept ()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write(发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write ()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的:应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
下面是我自己在linux里写的socket连接代码供大家参考:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERPORT 8000
#define SERIP "ip地址"
int main(int argc, char* argv[])
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in seraddr,cliaddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT);
int dst;
inet_pton(AF_INET,SERIP,(void*)&dst);
seraddr.sin_addr.s_addr = dst ;
int ret = bind(lfd,(struct sockaddr*)&seraddr,sizeof(seraddr));
listen(lfd,64);
socklen_t addrlen = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&addrlen);
//网络字节序整形IP地址转化成一个本地字节序点分十进制的字符窜IP
char clip[32];
inet_ntop(AF_INET,&cliaddr.sin_addr,clip,sizeof(clip));
printf("clien IP=%s,PORT=%d connect ok\n",clip,ntohs(cliaddr.sin_port));
char buf[1024];
while(1){
int rr = read(cfd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,rr);
write(cfd,buf,rr);
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERPORT 8000
#define SERIP "IP地址"
int main(int argc, char* argv[])
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT);
int dst;
inet_pton(AF_INET,SERIP,(void*)&dst);
seraddr.sin_addr.s_addr = dst ;
int ret = connect(lfd,(struct sockaddr*)&seraddr,sizeof(seraddr));
if(ret < 0){
perror("connect erro");
exit(1);
}
char buf[1024];
while(1){
write(lfd,"I LOVE EAT DOG SHI ",20);
int rr = read(lfd,buf,sizeof(buf));
write(1,buf,rr);
}
return 0;
}