一、socket网络编程
首先简单介绍一下UNIX/Linux下的socket:
在UNIX、Linux系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
你也许听过很多高手说过,UNIX/LLinux中的一切皆文件,那个家伙说的没错。
为了表示和区分已经打开的文件,UNIX/Linux会为每个文件分配一个ID,这个文件就是一个整数,被称为文件描述符(File Descriptor),例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符,你必须理解这句话:
我们可以通过 socket
() 函数来创建一个网络连接,或者说打开一个网络文件,socket
() 的返回值就是文件描述符(注意在windows下的socket返回的叫文件句柄,并不是叫文件描述符)。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read
() 读取从远程计算机传来的数据;
用 write
() 向远程计算机写入数据。
你看,只要用 socket
() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!
那么现在我们正式来介绍一下关于socket的相关知识;
1.TCP/UDP的区别
首先标准套接字分为TCP和UDP协议两种不同type的工作流程,TCP网络编程相对于UDP来说相对复杂,因为TCP是面向连接的服务,其中包括三次握手建立连接的过程,而UDP则是无连接的服务。
(1)TCP面向连接(如打电话要先拨号建立连接),而UDP是无连接的,即发送数据之前不需要建立连接。
(2)TCP提供可靠的服务,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;而UDP则是尽最大努力进行交付,即不保证可靠交付。
(3)TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;而UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(这样对实时应用很有用,如,IP电话,实时视频会议等)。
(4)每一条的TCP只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信
(5)TCP首部开销20字节;UDP首部开销小,只有8个字节。
(6)TCP的逻辑通信的信道是全双工的可靠信道,而UDP则是不可靠信道。
接下来看一下TCP和UDP的两种模式的流程区别
TCP:服务端和客户端的通信流程:
UDP:服务端和客户端的通信流程:
由上图可以看出,UDP与TCP的区别是UDP中是少了连接的过程。
2.那么介绍完TCP/UDP后,我们再来看一下IP地址跟端口号
IP地址:IP地址是网络中主机(电脑)的标识,在网络中主机想要与其他机器通信就必须拥有一个自己的IP地址,IP地址为32位(IPV4)或者128位(IPV6),每一个数据包都必须携带目的地址IP和源IP地址,路由器依靠此信息为数据包选择最优路由(路线)。(IP地址就有点像是几栋楼中的楼的号码)
端口号:用于区分一台主机中接收到的数据包应该交给那一个进程进行处理(注意TCP和UDP的端口号是相互独立的)(如果IP地址是相当于一栋楼的楼号的话,那么端口号就相当于是这栋楼里面的房间的房号)
那么来介绍一下端口号的作用是什么;
一台拥有IP地址的主机可以提供许多服务,比如Web服务,FTP服务,SMTP服务等。
这些服务完全是可以由一个IP地址来实现的,那么,主机是怎么样区分不同的网络服务的呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。
实际上通过"IP地址+端口号"来区分不同的服务的
端口提供了一种访问通道,服务器一般都是通过知名的端口来识别的。例如,对于每个TCP/IP的实现来说,FTP服务的端口号是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
3.接下来我们来看一下什么是字节序
字节序:是指多字节数据的存储顺序,在设计计算机系统的时候,有两种处理内存中数据的方法:即大端格式、小端格式。
小端格式(Little-Endian):将低位字节数据存储在低地址。
大端格式(Big-Endian):将高位字节数据存储在低地址。
接下来看一个图就基本理解的了
接下来就简单的举个例子吧,对于整型的0x12345678,它在大端格式和小端格式中的系统的存储方式如下图所示:
网络上的数据流是字节流,对于一个多字节数值,在进行网络传输的时候,先传递那个字节呢?也就是说,当接收端接收到第一个字节的时候,它是将这个字节当做低位还是高位来处理呢?
网络字节序的定义:将收到的第一个字节的数据当做高位来看待,这就要求发送端的发送的第一个字节应该是高位。而在发送端发送数据时,发送的第一个字节是该数字在内存中起始地址对应的字节。可见多字节数值在发送前,在内存中数值应该以大端法存放。
所以,网络协议指定了通讯字节序:大端。只有在多字节数据处理时才需要考虑字节序,运行在同一台计算机上的进程相互通信时,一般不用考虑字节序,异构计算机之间进行通讯时,需要将自己的字节序转换为网络字节序。
那么下面就介绍一下有关字节序转换的函数:
以下所有接口的头文件都是: #include
函数一:
函数原型:
uint16_t htons(uint16_t hostshort);
功能:
将16位主机字节序数据转换成网络字节序数据
参数:
hostshort:需要转换的16位主机字节序数据,uint16_t:unsigned short int
返回值:
成功返回网络字节序的值
函数二:
函数原型:
uint32_t htonl(uint32_t hostlong);
功能:
将32位主机字节序数据转换成网络字节序数据
参数:
hostlong:需要转换的32位主机字节序数据,uint32_t:32位无符号整型
返回值:
成功:返回网络字节序的值
函数三:
函数原型:
uint32_t ntohl(uint32_t netlong);
功能:
将32位网络字节序数据转换成主机字节序数据
参数:
netlong:需要转换的32位网络字节序数据;uint32_t:unsigned int
返回值:
成功:返回主机字节序的值
函数四:
函数原型:
uint16_t ntohs(uint16_t netshort);
功能:
将16位网络字节序数据转换成主机字节序数据
参数:
netshort:需要转换的16位网络字节序数据;uint16_t:unsigned short int
返回值:
成功:返回主机字节序的值
介绍完字节序转换函数后,那么现在我们来看一下地址转换函数:
首先我们先来介绍将字符串形式(即点分十进制)的地址转换成网络能识别的地址格式,
这里有inet_aton
和inet_pton
两个函数
两者的异同:
共同点:两者功能都是将点分十进制的IP地址转换成网络能识别的地址格式(即二进制)
不同点:(1)参数数量不同,使用上有小区别
(2)inet_aton
只适用于IPV4地址,inet_pton适用于IPV4和IPV6地址
(3)理论上讲前者是旧函数,后者是新函数
函数一:
int inet_aton(const char *cp, struct in_addr *inp);
功能:
将点分十进制字符串转换成32位无符号整数
参数:
cp:将被转换的点分十进制IP地址
inp:保存被转换成二进制的IP地址
返回值:
如果地址合法,则返回非0值,反之返回0值
函数二
函数原型:
int inet_pton(int af, const char *src, void *dst);
函数描述:
该函数将字符串src转换为af地址类型协议簇的网络地址,并存储到dst中。
对于af参数,必须为AF_INET
或AF_INET6
功能:
将 IPv4 和 IPv6 地址从点分十进制转换为二进制
参数:
af:
AF_INET
或AF_INET6
src:将被转换的点分十进制的IP地址
dst:保存被转换的二进制IP地址
返回值:
转换成功则返回1,对于指定的地址类型协议簇,如果不是一个有效的网络地址,
将转换失败,返回 0,
如果指定的地址类型协议簇不合法,将返回-1并,并且errno设置为EAFNOSUPPORT
在介绍完点分十进制的IP地址转换成二进制IP地址后,现在我们来介绍一下将二进制IP地址转换成点分十进制的IP地址的函数
这里有inet_ntoa
和inet_ntop
两个函数
两者的异同之处如上所示
函数三:
函数原型:
char *inet_ntoa(struct in_addr in);
函数描述:
inet_ntoa
()用来将参数in所指的大端网络字节序二进制的数字转换成ipv4点分十进制字符串网络地址,然后将指向此网络地址字符串的指针返回。成功则返回字符串指针,失败则返回NULL
。
功能:
将二进制IP地址转换成点分十进制的IP地址
参数:
in:将被转换的二进制IP地址(即32位无符号整数)
返回值:
成功:返回点分十进制的IP地址
失败:返回NULL
函数四:
函数原型:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
函数描述:
该函数将地址类型协议簇为af的网络地址src转换为字符串,并将其存储到dst中,其中dst不能是空指针。
调用者在参数size中指定可使用的缓冲字节数。
inet_ntop拓展自inet_ntoa来支持多种地址类型协议簇,inet_ntoa现在已经被弃用。
功能:
将二进制的IP地址转换成点分十进制的字符串(即点分十进制的IP地址)
参数:
af:
AF_INET
或AF_INET6
src:二进制的IP地址
dst:保存被转换成点分十进制的IP地址
size:指定可使用的缓冲字节数
返回值:
inet_ntop
执行成功,返回一个指向dst
的非空指针,如果执行失败,将返回NULL
,并且errno
设置为相应的错误类型。
错误类型:
(1)
EAFNOSUPPORT
af
并不是一个合法的地址类型协议簇
(2)ENOSPC
要转换的字符串地址src
其字节大小超过了给定的缓冲字节大小
有关IP地址转换的优秀文章:
IP地址转换的几个函数详解
以上就是对网络通信的一些相关知识的介绍,那么现在我们来介绍socket编程的相关函数
4.socket套接字相关函数的介绍
在介绍函数之前,我们先来认识一下socket编程服务端跟客户端的开发步骤是怎么样的,开发流程在TCP/UDP的区别那里有:
流程:
服务端:
1.创建套接字(socket)
2.将socket与IP地址和端口绑定(bind)
3.监听被绑定的端口(listen)
4.接收连接请求(accept)
5.从socket中读取客户端发送来的信息(read)
6.向socket中写入信息(write)
7.关闭socket(close)
客户端:
1.创建套接字(socket)
2.连接指定计算机的端口(connect)
3.向socket中写入信息(write)
4.从socket中读取服务端发送过来的消息(read)
5.关闭socket(close)
主要的开发流程了解了之后,我们来了解一下相关函数
1.函数说明:
socket
编程有以下几种基本函数:
socket
():用于创建套接字,同时指定协议和类型
bind
():将保存在相应地址结构中的地址信息与套接字进行绑定。主要用于服务器端,客户端创建的套接字可以不绑定地址
listen
():在服务器端建立套接字并绑定地址后,将套接字设置成监听模式(被动模式),准备接收客户端的连接请求
accept
():等待并接收客户端的连接请求。建立好TCP连接后,该函数将返回一个新的已连接套接字
connect
():客户端通过该函数向服务器端的监听套接字发送连接请求
send
()和recv
():通常用于TCP通讯中的发送和接收数据,也可用在UDP中
sendto
()和recvfrom
():通常用于UDP通讯中的发送和接收数据
2.函数原型:
(1)socket
函数
函数原型:
int socket(int domain, int type, int protocol);
返回值:成功返回非负套接字描述符,失败返回-1
参数:
domain
指明所使用的协议族,通常为AF_INET
,表示互联网协议族(TCP/IP
协议族);
AF_INET
IPv4
因特网域
AF_INET6
IPv6
因特网域
AF_UNIX
Unix
域
AF_ROUTE
路由套接字
AF_KEY
密钥套接字
AF_UNSPEC
未指定
type
参数指定socket
的类型
SOCK_STREAM
:
流式套接字提供可靠的,面向连接的通信流,它使用TCP
协议,从而保证了数据传输的正确性和顺序性
SOCK_DGRAM:
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠,无差错的。它使用数据报协议UDP
SOCK_RAW
:
允许程序使用底层协议,原始套接字允许对底层协议如IP
或ICMP
进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
protocol
参数
通常赋值为“0
”
0
选择type
类型对应的默认协议
IPPROTO_TCP
TCP
传输协议
IPPROTO_UDP
UDP
传输协议
IPPROTO_SCTP
SCTP
传输协议
IPPROTO_TIPC
TIPC
传输协议
(2)bind
函数
函数原型:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于绑定IP
地址和端口号到socket
返回值:成功返回0
,失败返回-1
参数:
sockfd
;
是一个socket
描述符
addr
:
是一个指向包含有本机IP
地址及端口号等信息的sockaddr
类型的指针,指向要绑定给sockfd
的协议地址结构,这个地址结构根据地址创建socket
时的地址协议族的不同而不同。
addrlen
:
地址的长度,一般用sizeof(struct sockaddr_in)
表示
在这里介绍一下IPV4
由上图可以看出我们在实际编程的时候通常是对第二个结构体进行使用,由于struct sockaddr
数据结构类型不方便设置,所以通常会通过对struct sockaddr_in
进行地质结构设置,然后进行强制类型转换成struct sockaddr
类型的数据。
(3)listen
函数
函数原型:
int listen(int sockfd, int backlog);
返回值:成功返回0
,失败返回-1
功能:
设置能处理的最大连接数,listen
并未开始接受连线,只是设置了socket
的listen
模式,listen
函数只用于服务器端,服务器进程不知道要与谁进行连接,因此,它不会主动的要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接,主要就连个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
内核为任何一个给定监听套接字维护两个队列:
未完成连接队列,每个这样的SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP
三次握手过程,这些套接字处于SYN_REVD
状态
已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项,这些套接字处于ESTABLISHED
状态;
参数:
sockfd
:
sockfd
是socket
系统调用返回的服务端socket
描述符
backlog
:
backlog
指定在请求队列中允许的最大的请求数,大多数系统默认为5
(4)accept
函数
函数原型 :
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:
该函数的返回值是一个新的套接字的描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符,一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP
三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
功能:
accept
函数由TCP
服务器调用,用于从已完成连接队列对头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠。
参数:
sockfd
:
sockfd
是socket
系统调用返回的服务器端socket
描述符
addr
:
用来返回已连接的对端(客户端)的协议地址
addrlen
:
客户端地址长度
(5)read
和write
函数
read
函数
函数原型:ssize_t read(int fd, void *buf, size_t count);
功能:从fd
所指的文件中读取count
个字节到buf
中
返回值:成功返回读取到的字节数,失败返回-1
参数:
fd
:将被读取的套接字描述符
buf
:用来保存被读取出来的字节流(即缓冲区)
count
:读取的字节数
温馨提示:read
() 是一个阻塞函数,如果客户端没有声明断开,那么它就会认为客户端仍旧可能发送数据,所以就会一直阻塞而不是返回-1;
write
函数
函数原型:ssize_t write(int fd, const void *buf, size_t count);
功能:将buf
中的count
个字节写入到fd
所指的文件中
返回值:成功返回写入的字节数,失败返回-1
参数:
fd
:将被写入的套接字描述符
buf
:将要被写入的内容
count
:写入的字节数
(6)数据收方的第二套API(send
和recv
函数)
send
函数
函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//包含3要素:套接字sockfd
,代发数据buf
,数据长度len
//函数只能对处于连接状态的套接字进行使用,参数sockfd
为已建立好连接的套接字描述符
//套接字描述符:即accept
函数的返回值
//参数buf
指向存放待发送数据的缓冲区
//参数len
为待发送数据的长度,参数flags
为控制选项,一般设置为0
recv
函数
函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//包含3要素:套接字sockfd
,接收缓冲区buf
,长度len
//函数recv
从参数sockfd
所指定的套接字描述符(必须是面向连接的套接字)上接收
//接收数据并保存到buf
所指定的缓冲区
//参数len
则为缓冲区长度,参数flags
为控制选项,一般设置为0
(7)connect
函数
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:该函数用于绑定之后的client
端(客户端),与服务器建立连接
返回值:成功返回0
,遇到错误时返回-1
,并且errno
中包含相应的错误码
参数:
sockfd
:目的服务器的socket
描述符
addr
:服务端的ip
地址和端口号的地址结构指针
addrlen
:地址的长度,通常被设置为sizeof(struct sockaddr)
3。介绍了那么多,终于来到实战阶段了
下面展示一个支持多客户端介入的服务端以及客户端
sever
.c
#include
#include
#include
#include
#include
#include
#include
#include
/*
struct sockaddr_in {
__kernel_sa_family_t sin_family;
__be16 sin_port;
struct in_addr sin_addr;
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
*/
int main(int argc,char **argv)
{
int sk_fd;
int cnet_fd;
int c_len;
int n_read;
char readBuf[128] = {'\0'};
char writeBuf[128] = {'\0'};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,'\0',sizeof(struct sockaddr));
memset(&c_addr,'\0',sizeof(struct sockaddr));
if(argc != 3)
{
printf("the param is not good!\n");
exit(-1);
}
//1.int socket(int domain, int type, int protocol);
sk_fd = socket(AF_INET,SOCK_STREAM,0);
if(sk_fd == -1)
{
perror("socket");
exit(-1);
}
//2.int bind(int sockfd, const struct sockaddr *addr,
// socklen_t addrlen);
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
bind(sk_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.int listen(int sockfd, int backlog);
listen(sk_fd,10);
//4.int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
c_len = sizeof(struct sockaddr_in);
while(1)
{
cnet_fd = accept(sk_fd, (struct sockaddr *)&c_addr.sin_addr, &c_len);
if(cnet_fd == -1)
{
perror("accept");
}
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0)
{
if(fork() == 0)
{
while(1){
//6.write
memset(writeBuf,0,sizeof(writeBuf));
printf("Please Input:");
gets(writeBuf);
write(cnet_fd,writeBuf,strlen(writeBuf));
}
}
while(1)
{
memset(readBuf,0,sizeof(readBuf));
//5.read
n_read = read(cnet_fd,readBuf,128);
if(n_read == -1)
{
perror("read");
}
else if(n_read == 0)
{
printf("the client is quit!\n");
break;
}
else{
printf("get %d message:%s\n",n_read,readBuf);
}
}
break;
}
}
return 0;
}
client
.c
#include
#include
#include
#include
#include
#include
#include
#include
/*
struct sockaddr_in {
__kernel_sa_family_t sin_family;
__be16 sin_port;
struct in_addr sin_addr;
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
*/
int main(int argc,char **argv)
{
int c_fd;
int c_con;
int n_read;
int n_write;
char sendBuf[128] = {0};
char readBuf[128] = {0};
struct sockaddr_in c_addr;
if(argc != 3)
{
printf("the param is not good!\n");
exit(-1);
}
//1.int socket(int domain, int type, int protocol);
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1)
{
perror("socket");
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//2.int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
c_con = connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in));
if(c_con == -1)
{
perror("connect");
exit(-1);
}
while(1)
{
if(fork() == 0)
{
while(1)
{
//3.write
memset(sendBuf,0,sizeof(sendBuf));
printf("Please Input:");
gets(sendBuf);
n_write = write(c_fd,sendBuf,strlen(sendBuf));
}
}
while(1)
{
//4.read
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd,readBuf,128);
if(n_read == -1)
{
perror("read");
}
printf("get %d from the sever:%s\n",n_read,readBuf);
}
}
return 0;
}
结果:
由以上结果可看出,服务端跟客户端在进行交流,其实可以多个客户端接入的
以上就是对linux下socket编程学习的一些总结
在这附上两遍关于socket编程的优秀博文
优秀博文1
优秀博文2
学习笔记,仅供参考