网络编程——TCP通信
linux网络编程
一,ip和端口的了解
- 什么是socket?
1). socket在英文中翻译为套接字,插座,排插。(因为插座种类非常多,就好像有很多协议一样,这些协议必须一致,才可以进行通信。)
网络编程 -> 套接字编程、socket编程。
2).socket()其实也是一个函数接口。
该函数功能: 就是创建一个套接字。
3).socket也指套接字 -> 特殊的文件描述符。
-
什么是IP地址?
例如: 192.168.14.2 -> 点分制
在一个局域网中,每一台主机都会有一个IP地址,而且IP地址必须是相同的网段 -> 指的是IP地址前3个数字一样。
每一个IP地址都是32位。 如果在网络编程的代码中使用到IP地址,那么就必须把这32位的数据转成网络字节。
(简单来讲:电脑与电脑之间的通信是需要一个地址的,有了地址,我才可以准确的跟小明的电脑通信,就相当于日常生活中的门牌号一样,有了门牌号,我就可以找到小明的家,假如没有IP地址/门牌号,我又怎么跟小明的电脑通信/怎么找到小明的家?) -
什么是端口号?
在一个局域网中,两台主机需要通信,除了IP地址需要处于同一个网段中,还需要使用相同的端口号。
(端口号其实就比如电脑上的QQ,端口号其实就是电脑上的应用的地址,因为咱们通过IP地址识别并找到相应的电脑主机还不够,还要找出对应的应用,QQ之间的通信其实就是相同的端口号的,那为什么QQ不能和微信之间通信呢,很明显,端口号不一样)主机A 主机B
IP地址相同网段: 192.168.14.2 192.168.14.4
相同的端口号: 5000 5000
端口号是16位。 如果在网络编程的代码中使用到端口号,那么就必须把这16位的数据转成网络字节。
系统用了端口: 0~1023 (用户不能使用)
用户有效端口: 1024~65535 (用户随便用) -> 端口号使用结束后,至少等待1分钟之后,才能再次使用。
- 如何查看和配置ubuntu的IP地址
自己查看其他资料,这里不必多说。
二,通信协议------TCP
-
TCP协议特点?
TCP协议全称: Tranmission Control Protocol
传输 控制 协议TCP协议通信类似于现实打电话,必须要连接上才能通信。 -> TCP协议是一种可靠的传输方式。
TCP协议作用场景: 账号登录,文件传输。 -
分析TCP协议原理。
TCP协议能够正常连接,原因是双方遵循三次握手协议。
1)第一次握手:
在建立TCP连接时,客户端发送syn包(seq=j)给服务器,客户端等待服务器确认。2)第二次握手:
服务器收到了syn包之后,必须确认客户端的SYN(ack=j+1),同时服务器也会发送一个SYN包(seq=k)给客户端,即SYN+ACK,此时服务器进入等待状态。3)第三次握手:
客户端收到服务器发过来的SYN+ACK包,想服务器发送确认包(ACK=k+1),此包发送完毕,客户端与服务器就连接上了。 -
1).创建TCP套接字
接口声明:int socket(int domain, int type, int protocol);参数:
domain:域。AF_INET/PF_INET:网际协议 AF_UNIX/PF_UNIX:本地协议,可写成AF_LOCAL/PF_LOCAL
type:类型。
SOCK_STREAM:流式套接字 SOCK_DGRAM:数据包套接字
protocol:协议。
一般为0返回值:
成功:待连接套接字
失败:-1备注:在网际协议中,选择流式套接字就代表TCP协议,选择数据包套接字就代表UDP协议,第三个参数protocol一般都不用。
2)绑定IP地址和端口号
接口声明:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数:
sockfd:待连接套接字
addr:包含本地地址(IP+PORT)的通用地址结构体的指针
addrlen:地址结构体大小返回值:
成功:0
失败:-1备注:
通用地址结构体的定义:
struct sockaddr
{
sa_family_t sa_family; -> 协议 -> 2
char sa_data[14]; -> IP、端口号..
};
特殊地址结构体 —— IPv4地址结构体:
struct sockaddr_in
{
u_short sin_family; // 地址族 -> 2
u_short sin_port; // 端口号 -> 2
struct in_addr sin_addr; // IPV4地址 -> 4
char sin_zero[8];
};
struct in_addr
{
in_addr_t s_addr; // 无符号32位网络地址 -> IP
};
3) 设置监听套接字
接口声明:int listen(int sockfd, int backlog);
参数:
sockfd:待连接套接字
backlog:最大同时接收连接请求个数
返回值:
成功:0,并将sockfd设置为监听套接字
失败:-1
4) 等待对端连接请求
接口声明:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:监听套接字
addr:通用地址结构体,用以存储对端地址(IP+PORT)
addrlen:参数addr的存储区域大小
返回值:
成功:已连接套接字(非负整数)
失败:-1
5) 从TCP套接字接收数据
接口声明: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:已连接套接字
buf:存储数据缓冲区
len:缓冲区大小
flags:接收标志,置为0即可以。
返回值:
成功:已接收字节数
失败:-1
6)将文本地址转化为二进制地址
接口声明:int inet_pton(int af, const char *src, void *dst);
参数:
af:地址族。AF_INET:IPv4地址
src:指向“点分式”IPv4或IPv6地址的指针,例如“192.168.1.100”
dst:类型为struct in_addr *的指针
返回值:
成功:1
失败:0代表地址与地址族不匹配,-1代表地址不合法
7)将二进制地址转化为文本地址
接口声明:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af:地址族。
AF_INET:IPv4地址
AF_INET6:IPv6地址
src:类型为struct in_addr *或者struct in6_addr *的指针
dst:地址缓冲区指针,缓冲区至少
size:地址缓冲区大小,至少要INET_ADDRSTRLEN或者INET6_ADDRSTRLEN个字节
返回值:
成功:dst
失败:NULL
8)连接对端监听套接字
接口声明:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:待连接套接字
addr:包含对端地址(IP+PORT)的通用地址结构体的指针
addrlen:地址结构体大小
返回值:
成功:0
失败:-1
- 简单的我们通过分析两段代码就可以理解解以上内容
服务器端(rose):
编译: ./rose 50000
rose是文件名,50000是端口号
#include "head.h"
int main(int argc,char *argv[]) // ./rose 50000
{
//1. 创建TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);//AF_INET:网际协议 SOCK_STREAM:流式套接字
printf("sockfd = %d\n",sockfd);
//2. 绑定IP地址,端口号
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //地址族,AF_INET:网际协议(ipv4地址)
srvaddr.sin_port = htons(atoi(argv[1])); //端口号 htons:将一个无符号短整型数值转换为网络字节序 atoi:将字符串转化为整型数
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址 /usr/include/linux/in.h //htonl:将主机的无符号长整形数转换成网络字节顺序
//INADDR_ANY:指的是服务器当前的IP地址
bind(sockfd,(struct sockaddr *)&srvaddr,len); //绑定IP+端口
//3. 设置监听套接字
//调用listen之前: sockfd -> 待连接的套接字
listen(sockfd,5);
//调用listen之后: sockfd -> 监听套接字
//4. 等待客户端的连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);//阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符
if(connfd >= 0)
{
printf("connfd = %d\n",connfd);
printf("new connection:%s\n",inet_ntoa(cliaddr.sin_addr));
}
//5. 读取套接字上的数据
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
//阻塞
recv(connfd,buf,sizeof(buf),0);//用来接收远程主机通过套接字sockfd发送来的数据,并把这些数据保存到数组buf中。
printf("from jack:%s",buf);
if(strncmp(buf,"fenshou",7) == 0)
{
break;
}
}
//6. 回收TCP套接字的资源
close(sockfd);
close(connfd);
return 0;
}
客户端(jack):
编译:./jack 192.168.14.11 50000
jack:该程序文件名
192.168.14.11:服务器的IP地址
50000:端口号(必须要跟服务器的端口号一样)
#include "head.h"
int main(int argc,char *argv[]) // ./jack 192.168.14.11 50000
{
//1. 创建TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d\n",sockfd);
//2. 准备rose的IP地址。
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //地址族
srvaddr.sin_port = htons(atoi(argv[2])); //端口号
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //设置好Rose的IP地址
//3. 发起连接
//connect调用之前: sockfd -> 待连接套接字
int ret = connect(sockfd,(struct sockaddr *)&srvaddr,len);
if(ret == 0)
{
printf("connect success!\n");
}
//connect调用之后: sockfd -> 已连接套接字
//4. 发送数据给rose
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin); //从键盘获取数据,并放到缓冲区buf当中
send(sockfd,buf,strlen(buf),0); //用来发送buf里面的数据到另外一个主机当中
//参数: 1.socket:指定发送端套接字描述符。 2.buf: 表示存放发送数据的缓冲区。 3.strlen 实际要发送的字节数, 4.第四个参数一般置零
//假如键盘中输入fenshou,就退出
if(strncmp(buf,"fenshou",7) == 0)
{
break;
}
}
//5. 回收TCP套接字的资源
close(sockfd);
return 0;
}
/*在main(int argc,char *argv[]);
argc表示的是执行的时候的参数的个数
argv[]表示的是执行参数当中的某一个
假如一个程序是my.exe,我在执行的时候这样执行./my ss yy
则argc为3,argv[0]则为my,argv[1]为ss,argv[2]为yy
同样在此代码当中16行17行当中的argv[1]和argv[2]参考第三行的注释
该文件执行的是./jack 192.168.14.11 50000,所以,从此就比较容易得出该代程序的原理
*/
- 当然以上代码只能实现客户端单向服务器发送数据
- 要实现双向能够收发数据,稍微涉及到一点点系统编程 > 线程知识,则有下面代码
服务器端:
#include "head.h"
void *func(void *arg)
{
int connfd = *(int *)arg;
//不断接收数据
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./server 50001
{
//1. 创建TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2. 绑定IP地址到套接字上
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 不断等待对方的连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(connfd > 0)
{
printf("new connection:%s\n",inet_ntoa(cliaddr.sin_addr));
}
//5. 创建子线程
pthread_t tid;
pthread_create(&tid,NULL,func,(void *)&connfd);
//6. 不断发送数据给客户端
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(connfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
close(sockfd);
close(connfd);
return 0;
}
客户端:
#include "head.h"
void *func(void *arg)
{
int sockfd = *(int *)arg;
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
recv(sockfd,buf,sizeof(buf),0);
printf("from server:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./jack 192.168.14.4 50000
{
//1. 创建TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d\n",sockfd);
//2. 准备rose的IP地址。
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //地址族
srvaddr.sin_port = htons(atoi(argv[2])); //端口号
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //设置好Rose的IP地址
//3. 发起连接
//connect调用之前: sockfd -> 待连接套接字
int ret = connect(sockfd,(struct sockaddr *)&srvaddr,len);
if(ret == 0)
{
printf("connect success!\n");
}
//connect调用之后: sockfd -> 已连接套接字
//创建线程
pthread_t tid;
pthread_create(&tid,NULL,func,(void *)&sockfd);
//4. 发送数据给rose
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(sockfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//5. 回收TCP套接字的资源
close(sockfd);
return 0;
}
网络编程——UDP通信
一、 通信协议。 – UDP协议
TCP -> Tranmission Control Protocol
UDP -> User Data Protocol
IP -> Internet Protocol
-
TCP协议与UDP协议差异?
TCP协议面向有连接情况,是一种可靠传输方式。 -> 必须要连接上,才能通信。
UDP协议面向无连接情况,是一种不可靠传输方式。-> 不需要连接也能发送数据。 -
如何实现?
UDP协议 -> 类似于写信。
二、实现过程。
-
服务器:
//1. 创建UDP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);//2. 绑定。
bind(sockfd,(struct sockaddr *)&srvaddr,len);//3. 等待对方的数据。
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);//4. 关闭
close(sockfd); -
客户端:
//1. 创建UDP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);//2. 直接发送数据即可。
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);//3. 关闭
close(sockfd); -
两个新东西我们要知道一下
-
从UDP套接字接收数据
接口声明:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);参数:
sockfd:UDP套接字
buf:储存数据缓冲区
len:缓冲区大小
flags:接收标志,与函数send的flags完全一致
src_addr:对端网络地址
addrlen:地址长度返回值:
成功:已接收字节数
失败:-1 -
向UDP套接字发送数据
接口声明:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);参数:
sockfd:UDP套接字
buf:即将发送的数据
len:数据的长度
flags:发送标志,与函数send的flags完全一致
dest_addr:对端网络地址
addr_len:地址长度返回值:
成功:已发送字节数
失败:-1
- 以下是UDP通信的代码
服务器端:
#include "head.h"
int main(int argc,char *argv[]) //./rose 50002
{
//1. 创建UDP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
//2. 绑定IP地址,端口号,地址族到UDP套接字中
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family = AF_INET;//地址族
srvaddr.sin_port = htons(atoi(argv[1]));//端口号
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址 /usr/include/linux/in.h
//绑定ip+端口
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 不断等待客户端给自己发送数据
char buf[100];
struct sockaddr_in cliaddr;
while(1)
{
//清空缓冲区
bzero(buf,sizeof(buf));
//等待对方的数据
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len); 。
printf("from jack:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//4. 回收
close(sockfd);
return 0;
}
客户端:
#include "head.h"
int main(int argc,char *argv[]) //./rose 192.168.14.4 50002
{
//1. 创建UDP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
//2 准备rose的地址。
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family = AF_INET;//地址族
srvaddr.sin_port = htons(atoi(argv[2]));//端口号
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);//设置好rose的地址
//3. 不断写信给rose。
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);//从键盘输入数据
//直接发送数据
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//4. 回收资源
close(sockfd);
}
网络编程——非阻塞IO
-
什么是IO模型?
网络编程对于数据输出输入方法: 阻塞IO,非阻塞IO,多路复用,信号驱动。 -
这几种IO模型原理是如何?
1)阻塞IO: 一直阻塞等待某一个套接字的数据到达,如果有数据,则读取并返回,如果无数据,就会一直等待。
read(fd);
recv(fd);
accept(sockfd);
以上的例子,在过去,我们都会觉得是函数阻塞,例如read函数,accept函数阻塞,但是这样想是错误,因为真正阻塞的是套接字/文件描述符,并不是函数本身具有阻塞的属性。因为fd是文件描述符 -> fd默认就是阻塞属性 -> read(fd) -> 阻塞
二、非阻塞IO
-
如果要使用非阻塞IO,思路是如何?
1)先创建一个文件描述符/套接字 -> 默认都是阻塞属性。
2)设置非阻塞的属性给文件描述符 -> 这时候,文件描述符就是非阻塞的。
3)再调用read()/accept()/recv()去处理这个文件描述符时,就会非阻塞。 -
如何设置非阻塞属性给文件描述符? -> fcntl() -> man 2 fcntl
头文件:
#include <unistd.h>
#include <fcntl.h>原型:
int fcntl(int fd, int cmd, … /* arg */ );参数:
fd: 需要设置属性的文件描述符cmd:
F_GETFL (void) -> void代表第三个参数不需要填
Get the file access mode and the file status flags; arg is ignored.
//获取当前文件描述符的属性
F_SETFL (int) -> int代表后面那个参数要填
Set the file status flags to the value specified by arg.
O_NONBLOCK -> 非阻塞属性
返回值:
成功:
F_GETFL 返回文件的属性
F_SETFL 返回0
失败:
-1
- 咱们通过一段代码来了解
服务器端
#include "head.h"
int main(int argc,char *argv[])
{
//1. 创建一个TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2. 绑定IP地址,端口号
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//sockfd -> 默认是阻塞属性
//4. 添加非阻塞属性给sockfd
int state;
state = fcntl(sockfd,F_GETFL); //state就是sockfd当前的属性。
state |= O_NONBLOCK; //state就是新属性了
fcntl(sockfd,F_SETFL,state);
//sockfd -> 具有非阻塞属性
//5. 等待客户端连接
struct sockaddr_in cliaddr;
bzero(&cliaddr,len);
int connfd;
while(1)
{
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(connfd > 0) //有人连接,就会返回成功
{
printf("connfd = %d\n",connfd);
printf("new connection:%s\n",inet_ntoa(cliaddr.sin_addr));
break;
}
else{ //没有人连接,就会返回失败
printf("connect error!\n");
}
}
close(sockfd);
close(connfd);
return 0;
}