目录
一、网络编程概述
为什么需要学习网络编程,之前讲到的进程间的各种通信方式都是依靠Linux内核,
进程间通信方式博文:进程间通信六种通信方式_◣星河◢的博客-CSDN博客
他们的缺点是只能单机运行,要想在多机之间进行信息交互,就需要网络通信。
网络编程的两个重要因素:
1、地址
当一个客户端想要连接服务器时就必须要知道服务器的地址。地址分为两部分IP地址和端口号。
- IP地址:IP地址用于确定服务器位于哪一台主机(哪个设备)。一台主机可以运行多个服务,如FTP服务、Web服务等
- 端口号:一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等 这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。 实际上是通过“IP地址+端口号”来区 分不同的服务的。 端口提供了一种访问通道, 服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69
2、协议:确定了双方之后,我们想要进行数据的交互,肯定要有事先约定好的协议,才能进行数据的识别。网络通信协议有很多种,如http、tcp、udp等等。
二、套接字
一种独立于协议的网络编程接口
介绍:
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序),各种进程使用这个相同的域互相之间用Internet协议簇来进行通信 [3] 。
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。
主要类型:
1.流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。
2.数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
3.原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。
工作流程:
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :
(1)服务器监听。
(2)客户端请求。
(3)连接确认 。
1.服务器监听
所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。
2.客户端请求
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求 。
3.连接确认
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求 。
三、字节序
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
字节序又分为小端字节序和大端字节序。
- 小端字节序:低地址存高字节,高地址存低字节(将低序字节存储在起始地址),如:TCP/IP协议字节序(网络字节序)
- 大端字节序:低地址存低字节,高地址存高字节(将高序字节存储在起始地址),如:X86系列CPU字节序(计算机内部处理)
字节序的意义:在进行数据交换时,两个设备的字节序可能是不一样的,直接通信必然会造成数据的损坏,所以传输前要先进行字节序转换。
高低字节:例如0x12345678,左边字节为高字节,右边字节为低字节,0x12为高字节,0x78为低字节
对于数据 0x12345678,假设从地址0x4000开始存放,在大端和小端模式下,存放的位置分别为:
内存地址 小端模式 大端模式
0x4003 0x12 0x78
0x4002 0x34 0x56
0x4001 0x56 0x34
0x4000(低地址) 0x78 0x12
采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。
小端存储后:0x78563412 大端存储后:0x12345678
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址存高字节,高地址存低字节。
字节序转换API (端口号: 主机字节序转换成网络字节序)
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //将无符号整数hostlong从主机字节顺序转换为网络字节顺序
uint16_t htons(uint16_t hostshort); //将无符号短整数hostshort从主机字节顺序转换为网络字节顺序
uint32_t ntohl(uint32_t netlong); //将无符号整数netlong从网络字节顺序转换为主机字节顺序
uint16_t ntohs(uint16_t netshort); //将无符号短整数netshort从网络字节顺序转换为主机字节顺序
h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取
四、Sockt服务器和客户端的开发步骤
1、socket()函数
函数原型
//#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
参数:
domain 指定所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族)
AF_INET IPv4 因特网
AF_INET6 IPv6 因特网
AF_UNIX Uinix域
AF_ROUTE 路由套接字
AF_KEY 密钥套接字
AF_UNSPEC 未指定type 指定socket类型:
SOCK_STREAM 指定TCP协议
字节流套接字提供可靠的、面向连接的通信流;他使用TCP协议,从而保证了数据传输正确性和顺序性
SOCK_DGRAM 指定UDP协议
数据报文套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,使用UDP协议
SOCK_RAW 指定底层协议
允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用不便,主要用于一些协议的开发- 项目protocol通常赋值为0(让它3自动分配)
0 选择type类型的默认协议TCP协议
IPPROTO_TCP TCP传输协议
IPPPOTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议
IPPROTO_TIPC TIPC传输协议返回值:
成功返回一个socket文件描述符,失败返回-1
注意:上面的type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
2、bind()函数
添加IP地址和端口号。
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:是一个socket描述符
addr:是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。
addrlen :对应的是地址的长度。结构体大小
返回值
成功返回0 ,失败返回-1,errno被设置
ipv4对应的是
struct sockaddr {
sa_family_t sa_family; //协议族
char sa_data[14]; //IP地址+端口号
};
同等替换:struct sockaddr_in {
__kernel_sa_family_t sin_family; //协议族
__be16 sin_port; //端口号
struct in_addr sin_addr; //IP地址结构体unsigned char sin_zero[8];/* 填充 没有实际意义 只为了跟sockaddr结构在内存中对齐才能相互转换 */
};
如何查找struct sockaddr_in?
1.进入头文件文件夹cd /usr/include/
2.搜索当前文件夹下显示行数不区分大小写递归查找grep 'struct sockaddr_in {' * -nir
n显示行数,i不区分大小写,r 递归的(包括子目录) *表示当前目录的所有文件
地址转换API
/*将点分十进制的字符串转换为32位网络字节顺序的IP地址*/
char* inet_aton(const char* cp,struct in_addr in);
/*将32位网络字节顺序的IP地址转换为点分十进制的字符串*/
char* inet_ntoa(struct in_addr in);
3、listen()函数
将socket套接字变为监听套接字,准备接受客户端的连接。
//#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能
- 设置能处理的最大连接数,listen()并未开始接受连线,只是设置sockect的 listen模式,listen函故只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
- 内核为任何一个给定监听套接字维护两个队列:
. 未完成连接队列,每个这样的' SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TcP三次握手过程。这些套接字处于SYw_REVD状态;
. 已完成连接队列,每个已完成 TCP三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED状态;
参数
- sockfd
sockfd是 socket系统调用返回的服务器端socket描述符.
- backlog
backlog 指定在请求队列中允许的最大请求数
4、accept()函数
功能:与客户端建立连接,如果已完成请求的队列为空将阻塞到下一个请求到来。
接受一个客户端的连接请求,并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。
函数原型
//#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd: 已经绑定服务器端口地址信息的socket套接字
- addr:用来存储接受到的客户端的端口和地址
- addrlen:客户端地址信息的长度
返回值
- 该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TC三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
5、read、write等函数
字节流读取函数
- read()/write()
- recv()/send()
- readv()/writev()
- recvmsg()/sendmsg()
- recvfrom()/sendto()
//read()、write() 已连接状态使用
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
//函数均返回读或写的字节个数,出错则返回-1
第一个将buf中的nbytes个字写入到文件描述符f中,成功时返回写的字节数。
第二个为从f中读取 nbyte个字节到buf中,返回实际所读的字节数。
详细应用说明参考使用read write读写socket(套节字)。
//recv()、send() 已连接状态使用
#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);
//使用与read、write类似, flags控制选项常设置为0
//readv()、wirtev() 已连接状态使用
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /*指向一个缓冲区,这个缓冲区是存放readv()所接收的数据或 //writev()将要
发送的数据*/
size_t iov_len; /*接收的最大长度以及实际写入的长度*/
};
//iovcnt 为iov大小
//recvmsg()、sendmsg()
未连接状态可使用#include <sys/types.h>
#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
//recvfrom()、sendto()
未连接状态可使用#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
前三个多用于TCP 后两个多用于 UDP
6、close()函数
关闭某socket套接字,不过close函数并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
#include<unistd.h>
int close(int fd);
函数参数:
fd: 待关闭的socket
返回值:
成功返回0,失败返回-1
7、connect()函数
功能:该函数用于绑定之后的客户端,与服务器建立连接
函数原型
//#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
- sockfd
是目的服务器的sockect描述符
- addr
是服务器端的IP地址和端口号的地址结构指针
- addrlen
地址长度常被设置为sizeof(struct sockaddr)
返回值
- 成功返回0,遇到错误时返回-1,并且 errno中包含相应的错误码
五、示例
服务端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
char readBuf[128];
int n_read;
char msg[128] = {0};
int mark = 0;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if(argc != 3){
printf("param is no good\n");
exit(-1);
}
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
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(struct sockaddr_in));
listen(s_fd, 10);
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if(c_fd == -1){
perror("accept");
}
mark++;
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){
if(fork() == 0){
while(1){
sprintf(msg, "welcome No.%d clinet", mark);
write(c_fd, msg, strlen(msg));
sleep(3);
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("get massage: %d, %s\n",n_read,readBuf);
}
}
break;
}
}
return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
int c_fd;
char readBuf[128];
int n_read;
char msg[128] = {0};
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
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(struct sockaddr_in)) == -1){
perror("connect");
exit(-1);
}
while(1){
if(fork() == 0){
while(1){
memset(msg, 0, sizeof(msg));
printf("input: ");
gets(msg);
write(c_fd, msg, strlen(msg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("get massage from server: %d, %s\n",n_read,readBuf);
}
}
}
return 0;
}