网络:
地址:
数据 :
Socket 套接字:
1.TCP : 面向连接 如:A 打电话 B (可靠)
2.UDP: 面向报文 如:A 发短信给 B 数据量大 (不可靠)
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前 不需 要建立连接
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、 SMTP服务等
这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。
实际上是通过“IP地址+端口号”来区 分不同的服务的。端口提供了一种访问通
端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69
2.1 字节序概念:
字节序是指多字节数据在计算机内存中存储或者网络传输时各自字节的存储顺序。
2.2 常见序:
我们知道在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。
文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4…
Linux内核对所有打开的文件有一个文件描述符表格,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表这个数组的下标,数组的内容就是指向一个个打开的文件的指针。
网络编程场景模拟:
假如我是 客户端 ,我正要 去朋友家(服务器端),不知道他家具体位置,我的面前有5个楼,我要去他家怎么办呢?
正在这时候,我的朋友突然喊我,他用的 汉语(TCP/UDP) 叫我名字,他说到:我在 XX号楼(IP地址) ,XX号房间(端口号),我就很明确我要去访问的地方 。他就在房间里 等待我敲门(监听) ,然后我俩就 问他玩游戏吗(请求数据) ,他回到,上号!(回应数据),玩完后,我离开离他家(结束连接),我走后他也去上班了。
1.socket ():创建套接字
2.bind() :为套接字添加信息(IP地址和端口号)
3.listen() ; 监听网络连接
4.accept():监听到有客户端接入,接受一个连接
5. 数据交互
6.close() : 关闭套接字,断开连接。
int socket(int domain ,int type ,int protocol);
参数说明:
参数1:domain
domain : 指明所使用的协议族
通常为AF_INET,表示互联网协议族(TCP/IP协议族);
AF_INET IPv4 | 因特网域 |
AF_INET IPV6 | 因特网域 |
AF_UNIX | Unix域 |
AF_ROUTE | 路由器套接字 |
AF_KEY | 密钥套接字 |
AF_UNSPEC | 未指定 |
参数2:type
type: 指定socket的类型:
参数3:protocol
protocol :通常赋值“0 ”
0 选择 type 类型对应的默认协议
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
地址准备好:
bind()函数:IP号端口号相应描述字赋值函数
#include
#include
int bind(int sockfd , const struct sockaddr *addr , socklen_t addrlen);
功能: 用于绑定IP地址和端口号到socketfd
参数说明:
sockfd : 是一个socket描述符
addr :是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同
//ipv4 对应的是:
struct sckaddr{
unisgned short as_family; //协议族
char sa_data[14]; //IP+端口
};
同等替换上面代码:
struct scockaddr_in {
sa_family_t sin_family; //协议族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址结构体
unisgned char sin_zero[8];
//填充,没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换
};
int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);
把字符串形式的“192.168.1.123”转为网络能识别的格式
char* inet_ntoa(struct in_addr inaddr);
把网络格式的IP地址转化为字符串形式
listen()函数 : 监听设置函数
#include
#include
int listen(int sockfd, int backlog);
功能:
参数说明:
sockfd: sockfd是socket系传调用返回的服务器端socket描述符
backlog: backlog指定在请求队列中允许的最大请求数
accept()函数
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
accept 函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠
参数
sockfd: sockfd是socket系统调用返回的服务器端socket 描述符,是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在
addr: 用来返回已连接的对端(客户端)的协议地址
addrled: 客户端地址长度
返回值
该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,accept函数接受一个客户端请求后会返回一个新的SOCKFD值,当有不同的客户端同时有不同请求时,会返回不同的SOCKFD的值。这个不同的值和建立SOCKET 时生成的SOCKFD还是不同的。服务器与客户端之间的通信就是在这些不同的SOCKFD上进行的。
而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
字节流读取函数
在套接字通信中进行字节读取函数: read() , write()。与 I/O中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。
ssize_t write(int fd, const void*buf,sizet nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);
返回值: 读或写的字节个数,出错则返回-1
参数说明:
write 参数 :将buf中的nbytes个字节写入到文件描述符fd中,成功时返回写的字节数。
read 参数:为从fd中读取 nbyte个字节到buf 中,返回实际所读的字节数。
详细应用说明参考使用read write 读写socket(套节字)。
网络I/O还有一些函数
如: recv()/send(),readv()/writev(),recvmsg()/sendmsg(),recvfrom()/sendto() 等
在TCP套接字上发送数据函数:有连接
ssize_t send(int s,const void *msg,size_t len,int flags);
//包含3要素:套接字s,待发数据msg,数据长度len
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0
在TCP套接字上接收数据函数:有连接
ssize_t recv(int s,void *buf,size_t len,int flags);
//包含3要素:套接字s,接收缓冲区buf,长度len
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0
connect()函数:客户机连接主机
#include
#include
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能
该函数用于绑定之后的client 端(客户端),与服务器建立连接参数
参数说明:
sockfd:是目的服务器的 sockect 描述符
addr :是服务器端的IP 地址和端口号的地址结构指针
addrlen:地址长度常被设置为sizeof(struct sockaddr)
返回值:
成功返回0,遇到错误时返回-1,并且errno 中包含相应的错误码
服务器建立——可连接代码:
#include
#include
#include
//#include
#include
#include
#include
#include
int main()
{
int s_fd;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
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(8989);
inet_aton("127.0.0.1", &s_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10); //不断监听外面是否有数据 ,listen配置完后,马上会结束
int clen = sizeof(struct sockaddr_in);
//4.accept
int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen);
//通过套接字连上客户端,后续对连接选项都是用返回值 c_fd 来操作,s_fd 可能还需要接收其他人连接。
if(c_fd == -1){
perror("accept");
}
printf("get connect%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
//6.write
//7.close
printf("connect\n");
while(1);
return 0;
}
注意1 :
在 cd /usr/include/ 下收索结构体 struct sockaddr_in grep “struct
sockaddr_in {” *-nir
*-nir表示在当前目录下递归的 找,r是递归 n是显示行号,i是不区分大小写
经过查找,在头文件加上#include
注意2:
s_addr.sin_port =(8888); 端口号用户要在5000以上 ,端口号要传到网络上去,所以我们要调用函数来变成网络字节序。
所以我们要知道字节序转换API
#include
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl( uint32_t net32bitvalue); //返回主机字节序的值
h代表host,n代表net,s代表short(两个字节), l代表long(4个字节),
通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY ,INADDR_ANY 指定地址让操作系统自己获取
listen(s_fd,10); //不断监听外面是否有数据 ,listen配置完后,马上会结束
int c_fd = accept(s_fd,NULL,NULL); //判断是否有已经三次握手的,如果有,就继续往下,跟客户端进行连接,把客户端信息返回到c_fd;
注意3:
inet_aton(“127.0.0.1”, &s_addr.sin_addr);
//把字符串形式的“127.0.0.1”转为网络能识别的格式
把本机地址转变为网络可识别的地址,需要调用函数来转换,
int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);
结果显示:
虚拟机的服务端正在等待连接。
在windows下用cmd 命令来连接服务端
连接成功!
我们调用的 socket(AF_INET ,SOCK_STREAM,0);
AF_INET 是TCP协议,我们在windows端cmd下telnet也是TCP协议,所以可以连接上。
printf(“get connect%s\n”,inet_ntoa(c_addr.sin_addr));
char* inet_ntoa(struct in_addr inaddr);
把网络格式的IP地址转化为字符串形式
int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen); 返回客户端信息,打印客户端IP地址
服务器建立——可连接 —— 交互 ——代码:
#include
#include
#include
#include
#include
#include
#include
int main()
{
int s_fd;
int n_read;
char readBuf[128];
char *msg ="Refuel.CONG";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
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(7777);
inet_aton("127.0.0.1", &s_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
int clen = sizeof(struct sockaddr_in);
//4.accept
int c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("get connet%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
n_read = read(c_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message:%d,%s\n",n_read,readBuf);
}
//6.write
write(c_fd,msg,strlen(msg));
//7.close
return 0;
}
#include
#include
#include
#include
#include
#include
#include
int main()
{
int k_fd;
int n_read;
char readBuf[128];
char *msg ="message from client \n";
struct sockaddr_in k_addr;
memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据
//1.socket
k_fd = socket(AF_INET ,SOCK_STREAM,0);
if(k_fd == -1){
perror("socket");
exit(-1);
}
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &k_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//3.connet
if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
//4. send
write(k_fd,msg,strlen(msg));
//5. read
n_read = read(k_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message from sever:%d,%s\n",n_read,readBuf);
}
//7.close
return 0;
}
打开服务端 ,服务端等待连接。。。打开客户端,连接成功,同时双端进行交互。
注意——连接的时候IP和端口号要一致:
//服务端
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &s_addr.sin_addr);
//客户端
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &k_addr.sin_addr);
在之前我讲进程那篇里,我们用输入的方式模拟了socket服务器对接客户端的应用场景
一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种情求达到时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
#include
#include
#include
#include
#include
#include
#include
int main(int argc , char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
char msg[] ={0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
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); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
int clen = sizeof(struct sockaddr_in);
//4.accept
while(1) //一直接收连接请求
{
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("get connet :%s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){ // 创建子进程 用来读取客户端
if(fork() == 0){ // 在建子进程,用来与客户端发送
while(1){
memset(msg,0,sizeof(msg));
printf("input(servel) :");
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 message: %d,%s\n",n_read,readBuf);
}
}
break;
}
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
int main(int argc ,char **argv)
{
int k_fd;
int n_read;
char readBuf[128];
char msg[] ={0};
struct sockaddr_in k_addr;
memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据
//1.socket
k_fd = socket(AF_INET ,SOCK_STREAM,0);
if(k_fd == -1){
perror("socket");
exit(-1);
}
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &k_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//3.connet
if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
//4. send
while(1){
if(fork() == 0){
while(1){
memset(msg,0,sizeof(msg));
printf("input(kehu) :");
gets(msg);
write(k_fd,msg,strlen(msg));
}
}
//5. read
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(k_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message from sever:%d,%s\n",n_read,readBuf);
}
}
}
//7.close
return 0;
}