网络通信编程
物理地址是标识适配器(网卡)的ID,六个字节,48位,全球唯一不会改变。作用是收发数据。
IP地址是用来表示一台主机的逻辑地址。
IPV4:4个字节,32位,用在局域网
IPV6:16字节,128位,用在广域网
IP将其分为子网ID和主机ID
子网ID:IP中被子网掩码中1连续覆盖的位
主机ID:IP中被子网掩码中0连续覆盖的位
比如:
IP:192.168.1.2/24(24代表连续24个1,即255.255.255.0)
子网id:192.168.1
主机id:2
网段地址:192.168.1.0
广播地址:192.168.1.255
可用的IP:2^8-2=254
127.0.0.1 称为回环地址 ,ping 127.0.0.1测试本地网络的连通性
Linux下设置ip:ifconfig ens33 192.168.1.2 netmask 255.255.255.0
桥接模式和本局域网相连,可以跟同个局域网内的所有主机通信
net网段通过虚拟网络和虚拟网卡通信,只能跟本网络的主机通信,不能和其他主机通信
协议:
- 应用层协议:
FTP: 文件传输协议
HTTP: 超文本传输协议
NFS: 网络文件系统 - 传输层协议:
TCP: 传输控制协议
UDP: 用户数据报协议 - 网络层:
IP:英特网互联协议
ICMP: 英特网控制报文协议 ping
IGMP: 英特网组管理协议 - 链路层协议:
ARP: 地址解析协议 通过ip找mac地址
RARP: 反向地址解析协议 通过mac找ip
端口
作用: 用来标识应用程序(进程)
端口需要固定,进程号无法固定所以不适用
一个程序可以有多个端口,但一个端口只能有一个应用程序
port: 2个字节 0-65535
0-1023 知名端口,FTP-21,HTTP-80,使用时一般需要root权限
自定义端口 1024 - 65535
netstat
进程间通信:
无名管道、命名管道、mmap、文件、信号、消息队列、共享内存
只能用于本机的进程间通信
不同的主机间进程通信方法: socket,socket必须成对出现
小端:低位存低地址,高位存高地址
大端:低位存高地址,高位存低地址
发送数据需要转大端的网络字节序,接收数据会将大端数据转成自己字节序
#include <arpa/inet.h>
// 括号里面为整型
// hton 小端->大端
// ntoh 大端->小端
// l 8个字节
// s 4个字节
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IP转换
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能: 将点分十进制串 转成32位网络大端的数据("192.168.1.2" ==> )
参数:
af :
AF_INET IPV4
AF_INET6 IPV6
src : 点分十进制串的首地址
dst : 32位网络数据的地址
成功返回1
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
功能: 将32位大端的网络数据转成点分十进制串
参数:
af : AF_INET
src : 32位大端的网络数 地址
dst : 存储点分十进制串 地址,数组大小定义为16就行,宏为INET_ADDRSTRLEN=16
size : 存储点分制串数组的大小
返回值: 存储点分制串数组首地址
网络通信解决三大问题:协议、ip、端口
ipv4套接字结构体
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order, 4 bytes*/
};
sin_family: 协议 AF_INET
sin_port: 端口
sin_addr: ip地址
ipv6套接字结构体
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */
__be32 sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
struct in6_addr {
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
通用套接字结构体
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
创建套接字API
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
创建套接字
参数:
domain: AF_INET
type: SOCK_STREAM 流式套接字 用于tcp通信
protocol: 0
成功返回文件描述符,失败返回-1
连接服务器
#include <sys/socket.h>
int connect(intsockfd , const struct sockaddr *addr, socklen_t addrlen);
功能: 连接服务器
sockfd: socket套接字(文件描述符)
addr: ipv4套接字结构体的地址
addrlen: ipv4套接字结构体的长度
客户端通信
#include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
//创建套接字
int sock_fd;
sock_fd = socket(AF_INET, SOCK_STREAM,0);
//连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.21.29", &addr.sin_addr.s_addr);
connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
//读写数据
char buf[1024]="";
while(1)
{
//发送数据给服务器
int n = read(STDIN_FILENO, buf, sizeof(buf));
write(sock_fd, buf, n);
//从服务器接收数据
n = read(sock_fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
printf("\n");
}1
//关闭
close(sock_fd);
return 0;
}
绑定套接字
给套接字绑定固定的端口和ip
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd: 套接字
addr: ipv4套接字结构体地址
addrlen: ipv4套接字结构体的大小
返回值:
成功返回0 失败返回;-1
监听连接
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数:
sockfd : 套接字
backlog : 已完成连接队列和未完成连接队里数之和的最大值 128
三次握手 未完成连接队列->已完成连接队列
提取连接
会创建新的套接字去通信,原套接字只监听有无新连接
#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
如果连接队列没有新的连接,accept会阻塞
功能: 从已完成连接队列提取新的连接
参数:
socket : 套接字
address : 获取的客户端的的ip和端口信息 iPv4套接字结构体地址
address_len: iPv4套接字结构体的大小的地址
socklen_t len = sizeof(struct sockaddr );
返回值:
新的已连接套接字的文件描述符
tcp服务器通信步骤
- 创建套接字 socket
- 绑定 bind
- 监听 listen
- 提取 accept
- 读写 read write
- 关闭 close
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//创建套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
// addr.sin_addr.s_addr = INADDR_ANY;//绑定的是通配地址
inet_pton(AF_INET,"192.168.21.37",&addr.sin_addr.s_addr);
int ret = bind(lfd,(struct sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
perror("");
exit(0);
}
//监听
listen(lfd,128);
//提取
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len);
char ip[16]="";
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,
ip,16), ntohs(cliaddr.sin_port));
//读写
char buf[1024]="";
while(1)
{
bzero(buf,sizeof(buf));
// int n = read(STDIN_FILENO, buf, sizeof(buf));
// write(cfd, buf, n);
int n = 0;
n = read(cfd, buf,sizeof(buf));
if(n == 0)//如果read返回等于0,代表对方关闭
{
printf("client close\n");
break;
}
printf("%s\n",buf);
}
//关闭
close(lfd);
close(cfd);
return 0;
}
连接语句:nc 192.168.21.37 8000
包裹函数
wrap.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))//如果是被信号中断和软件层次中断,不能退出
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)//如果是被信号中断,不应该退出
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取固定的字节数数据*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
/*:固定的字节数数据*/
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
int tcp4bind(short port,const char *IP)
{
struct sockaddr_in serv_addr;
int lfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&serv_addr,sizeof(serv_addr));
if(IP == NULL){
//如果这样使用 0.0.0.0,任意ip将可以连接
serv_addr.sin_addr.s_addr = INADDR_ANY;
}else{
if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
perror(IP);//转换失败
exit(1);
}
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
// int opt = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
return lfd;
}
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
TCP报文
• TCP头部为20字节
• 源端口号(16位)和目的端口号(16位):再加上Ip首部的源IP地址和目的IP地址可以唯一确定一个TCP连接
• 数据序号(16位):表示在这个报文段中的第一个数据字节序号
• 确认序号:仅当ACK标志为1时有效,确认号表示期望收到的下一个字节的序号
• 偏移:就是头部长度,有4位,跟IP头部一样,以4字节为单位。最大是60个字节
• 保留位:6位,必须为0
• 6个标志位:URG-紧急指针有效;ACK-确认序号有效;PSH-接收方应尽快将这个报文交给应用层;RST-连接重置;SYN-同步序号用来发起一个连接;FIN-终止一个连接。
• 窗口字段:16位,代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节
• 校验和:源机器基于数据内容计算一个数值,收信息机要与源机器数值结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。
MTU一般为1500,由网卡所能接受的最大帧大小所决定,只跟网卡有关。
三次握手中,第二次会包含窗口大小信息。
在信息通信的每次ACK报文中,每read一个数据报就会回一个ACK报文,并且都会包含win窗口大小。。
多进程(多个客户端)实现socket通信
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"
void free_process(int sig) // 回调函数
{
pid_t pid;
while(1)
{
pid = waitpid(-1, NULL, WNOHANG);//-1是任意子进程,WNOHANG没有子进程退出就不挂起的意思
if(pid <= 0 )// <0 子进程全部退出了, =0 没有子进程退出
{
break;
}
else
{
printf("child pid =%d\n",pid);
}
}
}
int main()
{
sigset_t set;//在创建信号之前可能子进程已经退出
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);//将SIGCHLD阻塞
int lfd = tcp4bind(8008,NULL);
Listen(lfd,128);
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
while(1)
{
char ip[16] = "";
int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);
printf("new client ip=%s port=%d\n",
inet_ptoi(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
pid_t pid = fork();
if(pid < 0)
{
perror("");
exit(0);
}
else if(pid == 0)
{
//关闭lfd
close(lfd);
while(1)
{
char buf[1024]="";
int n = read(cfd, buf, sizeof(buf));
if(n < 0)
{
perror("");
close(cfd);
exit(0);
}
else if(n == 0)//对方关闭j
{
printf("client close\n");
close(cfd);
exit(0);
}
else
{
printf("%s\n", buf);
write(cfd, buf, n);
// exit(0);
}
}
}
else //父进程
{
close(cfd);
//回收
//注册信号回调
struct sigaction act;
act.sa_flags =0;
act.sa_handler = free_process;
sigemptyset(&act.sa_mask);// 清空信号集
sigaction(SIGCHLD, &act, NULL); // SIGCHLD子进程消亡时发出的信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}