参考:socket是什么?套接字是什么?;Unix网络编程;socket文件描述符
文章中还有。。。。就不列出来了。
本文是对网上博客内容的一些摘抄与总结,谢谢各位的文章供我学习入门,侵删!
socket 原意为“插座”,可理解为电器插入插座便通上电,在网络编程中,socket被翻译为套接字,可理解为计算机通过它可以连接上因特网。
在Unix/linux中我们知道,万物皆为文件,具体地说,不同种类的类型都被抽象为文件,例如:普通文件,字符设备,块设备,套接字,进程等等。当一个文件被进程打开时,系统会为其创建一个文件描述符fd,这个fd是个整数,这时,文件的路径就成为了寻址系统,文件描述符则成为了字节流的接口。例如:用0表示标准输入文件,其对应的硬件设备为键盘;用1表示标准输出文件,其对应的硬件设备为显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个“桥梁”的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。
相对于普通文件这类真实存在于文件系统中的文件,tcp socket、unix domain socket等这些存在于内存中的特殊文件在被进程打开的时候,也会创建文件描述符。所以"一切皆文件"更准确的描述应该是"一切皆文件描述符"
如上所述,网络连接也是一个文件,它也有文件描述符,可以通过socket()函数创建一个网络连接,其返回值就是文件描述符,有了它,我们就可以用普通的文件操作函数来传输数据了,例如用 read() 读取从远程计算机传来的数据,用 write() 向远程计算机写入数据。
用于创建一个新的socket,用于客户端和服务端。成功返回一个文件描述符,失败返回-1。(linux中不记得函数形式可用man socket 查看参数及头文件)
int socket (int domain, int type, int protocol);
其主要作用是将由socket函数创建的文件描述符和一个本地地址结构体绑定起来。通俗点说,即给新买的手机插上电话卡。成功返回0,失败返回-1,其函数原型为:
int bind (int socketfd, const struct sockaddr *my_addr, socklen_t addrlen)
第一个参数为调用socket函数返回的文件描述符。
其中,第二个参数即为我们想要绑定的地址,可看到其为一个结构体指针,指向sockaddr这结构体。这个sockaddr为通用的套接字地址,其类型定义为:
上述结构体在sa_data中包含了ip,port等信息,考虑到系统的兼容性,一般采用另外一个结构体(struct sockaddr_in)来代替,这个结构体描述了internet环境下的地址形式(可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体):
注意到,上述结构体中嵌套了一个结构体
这点在具体程序实现中会碰到。
因为我们在tcpip中所需的地址及端口信息都为internet环境下的,所以先需要sockaddr_in存放ip地址和端口信息,后面在调用bind函数时将一个sockaddr_in{}类型的对象强制转为sockaddr{}类型,再赋值给bind的第二个参数。这两个函数的区别,具体可参考:sockaddr和sockaddr_in详解
struct sockaddr与struct sockaddr_in的区别和联系
结构体成员分析
sin_family:协议簇
sin_port:16位端口号
sin_addr:32位地址信息,以网络字节序保存
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。用memset函数写入0进行初始化即可。
void *memset(void *s,int c,size_t n)
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。linux可用man memset查看参数及头文件。
下面说明以下在这个过程中用到的具体的函数
注意:port为unsigned short 16位,addr为 unsigned long 32位,据此选择对应的函数即可。
具体可参考文章:网络字节序和主机字节序,网络字节序和主机字节序
网络字节序和主机字节序详解!!!
不同的机器有不同的字节序类型。考虑一个16位整数,由2个字节组成,内存中存储这个整数的顺序有两种,一种是将低序字节存储在起始地址,称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,称为大端(big-endian)字节序。
这两种字节序没有统一的标准,两种格式都有机器在使用,比如,Inter x86、ARM核采用的是小端模式,Power PC、MIPS UNIX和HP-PA UNIX采用大端模式。
在数据传输过程中,一定有一个标准化的过程,例如安卓手机充电器接口。也就是说,主机a到主机b的通信,一定是服从:
网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用大端排序方式
主机字节序:特定主机内的内存的数据处理方式。
对应过来,就有:
第三个参数addrlen是指第二个参数在sockaddr{}类型下的实际长度,可用sizeof函数。
一般在基于流式套接字的服务中会有这一步?,成功返回0,失败返回-1,函数原型为;
int listen(int listenfd, int backlog)
该函数的主要作用是将sockfd变为被动的连接的监听套接字。
第一个参数:一般来说,socket函数可以创建一个套接字。默认情况,内核会认为socket函数创建的套接字是主动套接字(active socket),它存在于一个连接的客户端。而服务器调用listen函数告诉内核,该套接字是被服务器而不是客户端使用的,即listen函数将一个主动套接字转化为监听套接字(以 listenfd 表示)。监听套接字可以接受来自客户端的连接请求。关于主动套接字和被动套接字的区别:监听套接字与已连接套接字
第二个参数:backlog指明那些已经经过了TCP三次握手的处于established状态的连接项在被系统调度前的最大排队等候数。换句话说,典型的服务器程序可以同时服务于多个客户端,服务器端调用listen函数来声明listenfd处于监听状态,并且最多允许有backlog个tcp连接。
该函数用于客户端主动向服务器发起连接,成功返回0,失败返回-1.函数原型为:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
参数情况与前面类似,sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是指向一个套接字地址结构的指针和该结构的大小。
对于TCP套接字,在调用该函数时会激发tcp三次握手的过程。
其作用是返回一个新的套接字的文件描述符来和客户端通信,serv_addr保存了客户端的IP地址和端口号,而 listenfd 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。成功返回0,失败返回-1,函数原型为:
int accept(int listenfd, struct sockaddr *addr,int *addrlen)
当accept被调用时,服务器程序会一直阻塞,直到有一个客户端发起连接。accept成功时,返回最后的服务器端的文件描述符,失败返回-1。
可通过man read 查看函数参数和头文件。其函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
收发数据的套接字内部有缓冲(buffer), 简言之就是字节数组. 通过套接字传输的数据将保存到该数组。因此, 我们 read、write其实是读取缓冲区的内容。
这两个函数分别由fd指定的socket套接口发送(接收)count字节的数据,然后存在buf缓冲区里面。返回值为实际发送成功的字节数。
调用该函数时对于TCP编程而言会触发一个FIN报文导致连接关闭。
可参考文章:为什么有监听socket和连接socket,为什么产生两个socket
从5中的描述上可以看出,accpet生成一个新的socket连接,返回该socket的文件描述符。对服务端来说,有两个socket,一个是用于监听的socket,还有一个就是客户端连接成功后,由accept函数创建的用于与客户端收发报文的socket。我觉得上面的文章总结的很到位:职责分工, 分层协作, 提高服务端性能。
基于上面的描述,我们就可以实现一个基本的tcp的客户端与服务端通信的代码。(新手注意不要在if语句后面加分号。可用printf函数或者gdb一步一步调试代码)
服务端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int listenfd;
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
return -1;
}
// printf("socket creat success\n");
struct sockaddr_in servaddr; //通过 man 7 ip 查看,按住键盘下,查看隐藏内容;
memset(&servaddr, 0, sizeof(servaddr));//函数用法上面解释过
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(listenfd,(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("bind");
return -1;
}
// printf("bind success\n");
if(listen(listenfd,5) < 0)
{
perror("listen");
return -1;
}
// printf("listen success\n");
//定义对方的地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
if((conn=accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen))<0)
{
perror("accept");
return -1;
}
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
//printf("%d",ret);
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
close(listenfd);
close(conn);
return 0;
}
客户端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sock;
if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
{
perror("socket");
return -1;
}
struct sockaddr_in servaddr; //通过 man 7 ip 查看,按住键盘下,查看隐藏内容;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// servaddr.sin_addr = hton1(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(connect(sock,(struct sockaddr*)&servaddr, sizeof(servaddr))<0)
{
perror("connect");
return -1;
}
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));
fputs(recvbuf,stdout);
}
close(sock);
return 0;
}
makefile文件
all: serve client
serve:serve.c
gcc -o serve serve.c
client:client.c
gcc -o client client.c