网络编程
网络常识//程序员需要知道关于网络的一些基本常识,最重要的是IP地址和端口;
网络编程: TCP 和 UDP;
ISO/OSI七层协议模型
=================================
7 应用层Application *** |
6 表现层Presentation *** |//Telnet/FTP/WWW等
5 会话层Session *** |
-- -- -- -- -- -- -- -- -- -- --
4 传输层Transport ### //TCP/UDP
--- --- --- --- --- --- --- --- --
3 网络层Network ### //IP和路由
----------------------------------
2 数据链路层Data Link ### |
1 物理层Physical ### |//网卡驱动
=================================
TCP/IP的模型
上三层,下四层;
5会话层/6表现层和7应用层统称为大应用层;
4传输层和3网络层不变;
1物理层和2数据链路层可以合也可以分开;
协议(Protocol)和协议簇(Protocol Family)
协议就是数据传送的方式和准则;
一系列相关的协议就组成协议簇;协议簇一般以最核心的协议命名;协议簇在某些资料上写成协议族;
常见协议
TCP Transmission Control Protocol传输控制协议,基于连接的服务;//传输层
IP Internet Protocol;Internet协议,信息传递机制;//网络层
UDP User Datagram Protocol用户数据报协议,无连接的服务;//传输层
HTTP 超文本传输协议;
FTP 文件传输协议(上传和下载);
收发邮件协议
...
消息包的逐层递增
Upper Layer Protocol #Msg#
TCP #Msg#@@
IP #Msg#@@&&
Enthernet **#Msg#@@&&**
一些Socket编程的概念
流stream
连接connection
阻塞block/非阻塞non-block
同步synchronous/异步asynchronous
IP地址
IP地址是计算机在网络中的唯一标识,可以定位网络中的计算机;
IP地址的本质是一个整数,分IPV4和IPV6,IPV4是主流,占32位(4个字节),IPV6占128位;每个Internet包必须带有IP地址;
IP地址有两种表示方式:
1.底层就是一个32位的整数,用8位十六进制数表示;
2.点分十进制;每个字节转成一个十进制数(255),中间用点分隔;
计算机更喜欢8位十六进制,而人更喜欢十进制,但在底层都是存储32位二进制;这两种表示方式可以进行转换;比如:
192.168.100.17和0xC0A86411是等价的;
0x: C0 A8 64 11
C语言也提供了转换函数;
ipconfig可以查看windows的IP地址,ipconfig/all可以查看物理地址;
ifconfig可以查看Unix/Linux的IP地址,ifconfig -a;
四级IP地址
class A: 0 NetWork(7bit) Local_Address(24bit) //是下面的总和
class B: 10 NetWork(14bit) Local_Address(16bit) //是下面的总和
class C: 110 NetWork(21bit) Local_Address(8bit) //是下面的总和
class D: 1110 Multicast_Address(28bit)
IP地址的工作原理
IP地址需要和网卡的物理地址绑定才能定位计算机,每块网卡的地址是出厂时就设定好的,无重复,叫物理地址(MAC地址/*machine*/);
子网掩码(Subnet Mask)
子网掩码用于区分两个IP是不是在同一个子网中:
166.111.160.1与166.111.161.45
子网掩码:255.255.254.0
IP和子网掩码做位与运算,结果一样的是一个子网;
166.111.160.1
255.255.254.0 (位与)
166.111.160.0
166.111.161.45
255.255.254.0 (位与)
166.111.160.0
因此166.111.160.1与166.111.161.45在同一个局域网中;
IP地址只能找到计算机(家),但对应不了计算机中的进程(门),端口号负责对应计算机中的某个进程;
端口
在网络技术中,端口(Port)大致有两种意思:
一是物理意义上的端口;比如,ADSL Modem/集线器/交换机/路由器用于连接其他网络设备的接口,如RJ-45端口/SC端口等等;
二是逻辑意义上的端口;一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等;
端口就是计算机中某个进程的网络标识;只有"IP+端口"才能进行数据交互;
端口也是一个整数,short 类型,范围0-65535;
端口分类:
0-1023//最好不要使用,系统使用其中的部分;
1024-48xxx//正常使用的端口,有很少一部分会被某些安装的软件使用,比如,1521 Oracle数据库;
48000以后的,动态端口,不稳定;
字节顺序(大端格式big endian/小端格式little endian)
不同的机器存储整数时,存放方式不同;比如,某个整数的4个字节分别是A B C D,有的机器是按A B C D方式存储,而有的机器是D C B A;
整数的存储有本机格式和网络格式之分;本机格式的字节顺序不确定,而网络格式的字节顺序是确定的;为保证端口号正确传输,需要把本机格式转成网络格式;
int x = 0x12345678;
再用 char 逐字节打印,就可以知道本机格式;
union的使用受系统大小端的影响
+----------------------+
| 大端格式 |
| int i = 1; |
| 0x1 0x0 0x0 0x0 |
|----------------------|
| 高地址 低地址 |
+----------------------+
+----------------------+
| 小端格式: |
| 低位数据放在低地址 |
| int i = 1; |
| 0x0 0x0 0x0 0x1 |
|----------------------|
| 高地址 低地址 |
+----------------------+
union C {
int i;
char c;
};
union C c;
c.i = 1;
printf("%d\n", c.c); //??
// 如果是小端格式,1会存储在低地址,结果返回1
// 如果是大端格式,1会存储在高地址,结果返回0
ping命令可以查看网络是否畅通,ping后面跟IP地址或域名;
网络中的域名(网址)和IP的关系:
上网应该是使用IP地址,因IP地址很难记忆,所以用域名(俗称网址)做助记,有专用的服务器负责把域名转成IP地址;
网络编程在C语言中叫socket编程;
Unix/Linux系统是作为服务器操作系统存在至今的,因此Unix的网络功能是非常全面和强大的;
网络编程编程其实有很成熟的套路,并且windows也能通用;
网络编程又叫socket编程,socket的本意是插座,用于网络交互;
socket编程分为本地通信和网络通信;本地通信由于其他IPC的存在,已经较少使用;重点就是网络通信;
网络编程至少要写两个程序,服务器端和客户端;因此编程时需要考虑两端的程序;
socket(套接字)编程步骤
1.服务器端
1.1创建一个socket,使用函数socket();
int socket(int domain, int type, int protocol);
参数:
domain域,用来选择协议簇
PF_UNIX PF_LOCAL PF_FILE //本地通信IPC
PF_INET //网络通信(IPv4)
PF_INET6 //网络通信(IPv6)
注上面的PF都可以写成AF
type用于选择通信类型
SOCK_STREAM //数据流(TCP)
SOCK_DGRAM //数据报(UDP)
protocol参数已经没有意义,因为协议已经被前两个参数决定,所以给0就可以了;
函数返回socket描述符,出错返回-1;
1.2准备通信地址(文件/IP和端口)
有三个通信地址相关的结构体:
struct sockaddr //不存数据,只做参数类型;
struct sockaddr_un //存储本地通信的数据;
本地通信使用的是一个文件做IPC媒介,因此存储了socket文件(.sock);
#include <sys/un.h>
struct sockaddr_un { //un是unix的缩写
int sun_family; //协议簇//sun是sockaddr_un的缩写
char sun_path[]; //文件名(带路径);
};
struct sockaddr_in //存储网络通信的数据
#include <netinet/in.h>
struct sockaddr_in {
int sin_family; //协议簇
short sin_port; //端口号
struct in_addr sin_addr; //IP地址//其实in_addr只有一个成员
};
1.3绑定(socket描述符和通信地址)
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bind(sockfd, (sockaddr*)(地址), sizeof(addr));
第2个参数需要强制类型转换为(sockaddr*);
绑定其实就是服务器对外开放了一个端口
1.4通信(read()/write())
使用读写文件描述符的方式读写socket描述符
1.5关闭socket描述符
2.客户端
步骤与服务器端一样;只需把第3步bind()绑定换成connect()连接即可;
注connect()的参数和bind()完全一样;
但bind()中提供的是服务器的IP和端口,connect()提供的是服务器端的IP和端口;自己的IP和端口封装在数据中发给服务器端,连接服务器时自己是不设防的;
如果服务器在读,客户端应该写;如果服务器端在写,客户端应该读;
//读写交互;
注使用网络编程时,网络通信地址中的IP和端口都需要使用转换函数
需要包含sys/socket.h和arpa/inet.h与netinet/in.h
in_addr_t inet_addr(const char *cp);
/* 点分十进制的IP转换为整数IP; */
uint16_t htons(uint16_t hostshort);
/* 端口,本机格式转换为网络格式host to network hostshort; */
还有一个整数转为点分十进制的函数
char *inet_ntoa(struct in_addr in);
以上写的是典型的1对1的模式,商业开发更多使用的是基于TCP和基于UDP的开发模式;
基于TCP协议的网络编程
TCP是有连接协议,所有操作基于客户端和服务器端且保持连接,会重发一切的错误数据;
TCP协议一对多编程步骤
1.服务器端//在以前基础上添加两步;
1.socket()获取一个socket描述符;
2.准备通信地址;
3.bind()绑定;
4.listen()监听;//可以设置当多个客户端同时访问时,只能操作一个,其他的放入队列中;listen()用于设置队列的最大长度;
5.等待客户端连接accept();
int accept(int sockfd, sockaddr* addr, socklen_t* addrlen);
参数:
sockfd 就是第一步的返回值;
addr是用于接收连接的客户端通信地址;
addrlen是传入传出参数;
先传入通信地址的大小,再传出获取到的客户端通信地址的大小;
返回新的描述符,用于和客户端的读写通信;
6.读写;对于第5步返回的描述符;
7.关闭两个socket描述符;//socket()和accept()各自返回的描述符;
2.客户端不需要做任何更改;
练习
改良服务器端的代码,改成可以为多个客户端服务的版本
加无限循环,退出时用信号2;
因为listen()负责监听,所以把listen()之后改为无限循环;
练习
改良程序,客户端要求能多次输入(scanf),并把客户端的每次输入发给服务器端,服务器端做转发(就是把客户端的输入发回给客户端);客户端输入bye退出;
客户端加循环,服务器也随之加;
客户端退出交互循环,服务器也退出交互循环;
UDP的编程
UDP协议是无连接协议,不保持连接;因此UDP不保证数据的完整和正确,因为不会重发错误数据;
TCP占用资源大,但能保证数据的正确完整;
UDP占用资源小,但不保证数据的正确完整;
UDP虽然是无连接协议,但是也可以建立UDP连接;
QQ是基于UDP的,大部分网络程序也是基于UDP的;
SOCK_STREAM //数据流(用于TCP);卖自来水
SOCK_DGRAM //数据报(用于UDP);卖瓶装矿泉水
UDP不需要connect(),它使用不同的数据发送函数和接收函数;
sendto()//发送,相当于connect()和write()的结合
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
成功返回发送数据的大小,失败返回-1;
sendto(sockfd, buf1, sizeof(buf1), 0,
(struct sockaddr*)&from, sizeof(from));
recvfrom()//接收
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
成功返回接收到的数据大小,失败返回-1;
socklen_t len = sizeof(from);
recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr*)&from, &len);
//注意最后一个参数与sendto的最后一个参数类型不同
无连接的UDP发送数据时无法使用write(),而接收数据时如不需要保留发送方通信地址可以使用read(),如果向直到发送方是谁需要使用recvfrom();
使用UDP做时间服务器
从sockaddr中取得Ip地址和端口号
在socket编程中,服务器端accept()等待一个客户端的连接,当连接成功后,accept拷贝客户端的地址信息到sin_addr里面,我们如何从sin_addr取得此客户端的Ip地址和端口号呢?
实际上,当sockaddr_in.sin_family = AF_INET时,sockaddr = sockaddr_in;
据此,我们可以做一下转换,就可以利用inet_ntoa()来得到ip地址和端口号:
网络常识//程序员需要知道关于网络的一些基本常识,最重要的是IP地址和端口;
网络编程: TCP 和 UDP;
ISO/OSI七层协议模型
=================================
7 应用层Application *** |
6 表现层Presentation *** |//Telnet/FTP/WWW等
5 会话层Session *** |
-- -- -- -- -- -- -- -- -- -- --
4 传输层Transport ### //TCP/UDP
--- --- --- --- --- --- --- --- --
3 网络层Network ### //IP和路由
----------------------------------
2 数据链路层Data Link ### |
1 物理层Physical ### |//网卡驱动
=================================
TCP/IP的模型
上三层,下四层;
5会话层/6表现层和7应用层统称为大应用层;
4传输层和3网络层不变;
1物理层和2数据链路层可以合也可以分开;
协议(Protocol)和协议簇(Protocol Family)
协议就是数据传送的方式和准则;
一系列相关的协议就组成协议簇;协议簇一般以最核心的协议命名;协议簇在某些资料上写成协议族;
常见协议
TCP Transmission Control Protocol传输控制协议,基于连接的服务;//传输层
IP Internet Protocol;Internet协议,信息传递机制;//网络层
UDP User Datagram Protocol用户数据报协议,无连接的服务;//传输层
HTTP 超文本传输协议;
FTP 文件传输协议(上传和下载);
收发邮件协议
...
消息包的逐层递增
Upper Layer Protocol #Msg#
TCP #Msg#@@
IP #Msg#@@&&
Enthernet **#Msg#@@&&**
一些Socket编程的概念
流stream
连接connection
阻塞block/非阻塞non-block
同步synchronous/异步asynchronous
IP地址
IP地址是计算机在网络中的唯一标识,可以定位网络中的计算机;
IP地址的本质是一个整数,分IPV4和IPV6,IPV4是主流,占32位(4个字节),IPV6占128位;每个Internet包必须带有IP地址;
IP地址有两种表示方式:
1.底层就是一个32位的整数,用8位十六进制数表示;
2.点分十进制;每个字节转成一个十进制数(255),中间用点分隔;
计算机更喜欢8位十六进制,而人更喜欢十进制,但在底层都是存储32位二进制;这两种表示方式可以进行转换;比如:
192.168.100.17和0xC0A86411是等价的;
0x: C0 A8 64 11
C语言也提供了转换函数;
ipconfig可以查看windows的IP地址,ipconfig/all可以查看物理地址;
ifconfig可以查看Unix/Linux的IP地址,ifconfig -a;
四级IP地址
class A: 0 NetWork(7bit) Local_Address(24bit) //是下面的总和
class B: 10 NetWork(14bit) Local_Address(16bit) //是下面的总和
class C: 110 NetWork(21bit) Local_Address(8bit) //是下面的总和
class D: 1110 Multicast_Address(28bit)
IP地址的工作原理
IP地址需要和网卡的物理地址绑定才能定位计算机,每块网卡的地址是出厂时就设定好的,无重复,叫物理地址(MAC地址/*machine*/);
子网掩码(Subnet Mask)
子网掩码用于区分两个IP是不是在同一个子网中:
166.111.160.1与166.111.161.45
子网掩码:255.255.254.0
IP和子网掩码做位与运算,结果一样的是一个子网;
166.111.160.1
255.255.254.0 (位与)
166.111.160.0
166.111.161.45
255.255.254.0 (位与)
166.111.160.0
因此166.111.160.1与166.111.161.45在同一个局域网中;
IP地址只能找到计算机(家),但对应不了计算机中的进程(门),端口号负责对应计算机中的某个进程;
端口
在网络技术中,端口(Port)大致有两种意思:
一是物理意义上的端口;比如,ADSL Modem/集线器/交换机/路由器用于连接其他网络设备的接口,如RJ-45端口/SC端口等等;
二是逻辑意义上的端口;一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等;
端口就是计算机中某个进程的网络标识;只有"IP+端口"才能进行数据交互;
端口也是一个整数,short 类型,范围0-65535;
端口分类:
0-1023//最好不要使用,系统使用其中的部分;
1024-48xxx//正常使用的端口,有很少一部分会被某些安装的软件使用,比如,1521 Oracle数据库;
48000以后的,动态端口,不稳定;
字节顺序(大端格式big endian/小端格式little endian)
不同的机器存储整数时,存放方式不同;比如,某个整数的4个字节分别是A B C D,有的机器是按A B C D方式存储,而有的机器是D C B A;
整数的存储有本机格式和网络格式之分;本机格式的字节顺序不确定,而网络格式的字节顺序是确定的;为保证端口号正确传输,需要把本机格式转成网络格式;
int x = 0x12345678;
再用 char 逐字节打印,就可以知道本机格式;
union的使用受系统大小端的影响
+----------------------+
| 大端格式 |
| int i = 1; |
| 0x1 0x0 0x0 0x0 |
|----------------------|
| 高地址 低地址 |
+----------------------+
+----------------------+
| 小端格式: |
| 低位数据放在低地址 |
| int i = 1; |
| 0x0 0x0 0x0 0x1 |
|----------------------|
| 高地址 低地址 |
+----------------------+
union C {
int i;
char c;
};
union C c;
c.i = 1;
printf("%d\n", c.c); //??
// 如果是小端格式,1会存储在低地址,结果返回1
// 如果是大端格式,1会存储在高地址,结果返回0
/*
* 大端格式小端格式验证
*/
#include <stdio.h>
int main(){
int x = 0x12345678;
char *p = (char *)&x;
printf("%x, %x, %x, %x\n", p[0], p[1], p[2], p[3]);
union C {
int i;
char c;
} c;
c.i = 1;
printf("1 == 0x%08x\n", c.c);
if (c.c == 1) {
printf("little endian\n");
} else {
printf("big endian\n");
}
return 0;
}
ping命令可以查看网络是否畅通,ping后面跟IP地址或域名;
网络中的域名(网址)和IP的关系:
上网应该是使用IP地址,因IP地址很难记忆,所以用域名(俗称网址)做助记,有专用的服务器负责把域名转成IP地址;
网络编程在C语言中叫socket编程;
Unix/Linux系统是作为服务器操作系统存在至今的,因此Unix的网络功能是非常全面和强大的;
网络编程编程其实有很成熟的套路,并且windows也能通用;
网络编程又叫socket编程,socket的本意是插座,用于网络交互;
socket编程分为本地通信和网络通信;本地通信由于其他IPC的存在,已经较少使用;重点就是网络通信;
网络编程至少要写两个程序,服务器端和客户端;因此编程时需要考虑两端的程序;
socket(套接字)编程步骤
1.服务器端
1.1创建一个socket,使用函数socket();
int socket(int domain, int type, int protocol);
参数:
domain域,用来选择协议簇
PF_UNIX PF_LOCAL PF_FILE //本地通信IPC
PF_INET //网络通信(IPv4)
PF_INET6 //网络通信(IPv6)
注上面的PF都可以写成AF
type用于选择通信类型
SOCK_STREAM //数据流(TCP)
SOCK_DGRAM //数据报(UDP)
protocol参数已经没有意义,因为协议已经被前两个参数决定,所以给0就可以了;
函数返回socket描述符,出错返回-1;
1.2准备通信地址(文件/IP和端口)
有三个通信地址相关的结构体:
struct sockaddr //不存数据,只做参数类型;
struct sockaddr_un //存储本地通信的数据;
本地通信使用的是一个文件做IPC媒介,因此存储了socket文件(.sock);
#include <sys/un.h>
struct sockaddr_un { //un是unix的缩写
int sun_family; //协议簇//sun是sockaddr_un的缩写
char sun_path[]; //文件名(带路径);
};
struct sockaddr_in //存储网络通信的数据
#include <netinet/in.h>
struct sockaddr_in {
int sin_family; //协议簇
short sin_port; //端口号
struct in_addr sin_addr; //IP地址//其实in_addr只有一个成员
};
1.3绑定(socket描述符和通信地址)
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bind(sockfd, (sockaddr*)(地址), sizeof(addr));
第2个参数需要强制类型转换为(sockaddr*);
绑定其实就是服务器对外开放了一个端口
1.4通信(read()/write())
使用读写文件描述符的方式读写socket描述符
1.5关闭socket描述符
2.客户端
步骤与服务器端一样;只需把第3步bind()绑定换成connect()连接即可;
注connect()的参数和bind()完全一样;
但bind()中提供的是服务器的IP和端口,connect()提供的是服务器端的IP和端口;自己的IP和端口封装在数据中发给服务器端,连接服务器时自己是不设防的;
如果服务器在读,客户端应该写;如果服务器端在写,客户端应该读;
//读写交互;
/*
* 本地通信服务器端
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
int main() {
//1 用socket()获取描述符
int sockfd = socket(PF_UNIX, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2 准备通信地址
struct sockaddr_un addr;
addr.sun_family = PF_UNIX; //本地通信
strcpy(addr.sun_path, "a.sock"); //会新建a.sock文件
//3 用bind()绑定
int res = bind(sockfd,
(struct sockaddr *)&addr, //强制类型转换一下
sizeof(addr));
// 判断返回值,如果返回-1表示失败;
if (res == -1) {
perror("bind"), exit(-1);
}
//4 通信
char buf[100] = { };
int i = 0;
for (i = 0; i <= 9; i++) {
int len = read(sockfd, buf, sizeof(buf));
printf("读到%d字节,内容:%s\n", len, buf);
sleep(1);
}
//5 关闭描述符
close(sockfd);
printf("读取结束\n");
return 0;
}
/*
* 本地通信客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
int main() {
//1 用socket()获取描述符
int sockfd = socket(PF_UNIX, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2 准备通信地址
struct sockaddr_un addr;
addr.sun_family = PF_UNIX; //本地通信
strcpy(addr.sun_path, "a.sock"); //会新建a.sock文件
//3 用connect()连接
int res = connect(sockfd,
(struct sockaddr *)&addr, //强制类型转换一下
sizeof(addr));
// 判断是否出错
if (res == -1) {
perror("connect"), exit(-1);
}
//4 读写
int i = 0;
for (i = 0; i <= 9; i++) {
int len = write(sockfd, "hello", 5);
printf("写入%d字节,%d\n", len, i);
sleep(1);
}
//5 关闭描述符
close(sockfd);
printf("发送完毕\n");
return 0;
}
注使用网络编程时,网络通信地址中的IP和端口都需要使用转换函数
需要包含sys/socket.h和arpa/inet.h与netinet/in.h
in_addr_t inet_addr(const char *cp);
/* 点分十进制的IP转换为整数IP; */
uint16_t htons(uint16_t hostshort);
/* 端口,本机格式转换为网络格式host to network hostshort; */
还有一个整数转为点分十进制的函数
char *inet_ntoa(struct in_addr in);
/*
* 本地网络编程服务端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr = "172,17.5.3"; //类型不兼容,不可以
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//IP地址为本服务器IP
//3绑定
int res = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (res == -1) { perror("bind"), exit(-1);}
//4通信
printf("通信已建立\n");
char buf[100] = { };
int res1 = read(sockfd, buf, sizeof(buf));
if (res1 == -1) {
perror("read"), exit(-1);
}
printf("已经读取%d字节,内容:%s\n", res1, buf);
//5关闭socket描述符
close(sockfd);
return 0;
}
/*
* 本地网络编程客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr = "";//不可以
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//IP地址为连接目标
//3连接
int res = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
printf("请输入一个字符串\n");
char buf[100] = { };
scanf("%s", buf);
write(sockfd, buf, sizeof(buf));
printf("已经写入\n");
//5关闭socket描述符
close(sockfd);
return 0;
}
以上写的是典型的1对1的模式,商业开发更多使用的是基于TCP和基于UDP的开发模式;
基于TCP协议的网络编程
TCP是有连接协议,所有操作基于客户端和服务器端且保持连接,会重发一切的错误数据;
TCP协议一对多编程步骤
1.服务器端//在以前基础上添加两步;
1.socket()获取一个socket描述符;
2.准备通信地址;
3.bind()绑定;
4.listen()监听;//可以设置当多个客户端同时访问时,只能操作一个,其他的放入队列中;listen()用于设置队列的最大长度;
5.等待客户端连接accept();
int accept(int sockfd, sockaddr* addr, socklen_t* addrlen);
参数:
sockfd 就是第一步的返回值;
addr是用于接收连接的客户端通信地址;
addrlen是传入传出参数;
先传入通信地址的大小,再传出获取到的客户端通信地址的大小;
返回新的描述符,用于和客户端的读写通信;
6.读写;对于第5步返回的描述符;
7.关闭两个socket描述符;//socket()和accept()各自返回的描述符;
2.客户端不需要做任何更改;
/*
* TCP网络编程服务端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
int main() {
printf("服务器 pid = %d\n", getpid());
//1 获取socket描述符
int sockfd = socket(PF_INET, SOCK_STREAM, 0); //用SOCK_STREAM//TCP
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2 准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222);
/* addr.sin_addr.s_addr = inet_addr("172.17.5.3"); */
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//解决地址被占用的问题address already in use
int reuseaddr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr,
sizeof(reuseaddr));
//3 bind()
int res = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (res == -1) {
perror("bind"), exit(-1);
}
printf("bind ok\n"); //bind()成功是一个里程碑,意味着服务器对外开放
//4 listen()
listen(sockfd, 100); //最多可以监听100+1个;
//5 accept()
struct sockaddr_in from; //用来存放连接到的客户端地址
socklen_t len = sizeof(from);
int fd = accept(sockfd, (struct sockaddr *)&from, &len);
//会等待客户端的连接;//accept()是阻塞函数
//如果客户端没有连接会一直等待;
if (fd == -1) {
perror("accept"), exit(-1);
}
printf("%s连接上了\n", inet_ntoa(from.sin_addr));
//6 读写
char buf[100] = { }, buf1[100] = { };
res = read(fd, buf, sizeof(buf));
if (res == -1) {
return -1;
}
printf("接收到内容client>%s\n", buf);
strcpy(buf1, "welcome");
write(fd, buf1, strlen(buf1)); //回发数据;
//7 关闭两个描述符
close(sockfd);
close(fd);
//close(sockfd);
return 0;
}
/*
* TCP网络编程客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr = "";//不可以
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//IP地址为连接目标
//3连接
int res = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
printf("请输入一个字符串\n");
char buf[100] = { };
scanf("%s", buf);
write(sockfd, buf, sizeof(buf));
printf("已经写入\n");
read(sockfd, buf, sizeof(buf));
printf("server:%s\n", buf);
//5关闭socket描述符
close(sockfd);
return 0;
}
练习
改良服务器端的代码,改成可以为多个客户端服务的版本
加无限循环,退出时用信号2;
因为listen()负责监听,所以把listen()之后改为无限循环;
练习
改良程序,客户端要求能多次输入(scanf),并把客户端的每次输入发给服务器端,服务器端做转发(就是把客户端的输入发回给客户端);客户端输入bye退出;
客户端加循环,服务器也随之加;
客户端退出交互循环,服务器也退出交互循环;
/*
* TCP网络编程服务端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
int sockfd;
void fa(int signo) {
printf("捕获到信号%d\n", signo);
if (signo == 2) {
printf("服务器即将关闭\n");
close(sockfd); //回收资源时释放sockfd;
sleep(1);
exit(0);
}
}
int main() {
signal(SIGINT, fa);
printf("按Ctrl+C退出服务器\n");
printf("服务器已启动 pid = %d\n", getpid());
//1 获取socket描述符
sockfd = socket(PF_INET, SOCK_STREAM, 0); //用SOCK_STREAM//TCP
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2 准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222);
addr.sin_addr.s_addr = inet_addr("172.17.5.3");
//解决地址被占用的问题address already in use
int reuseaddr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr,
sizeof(reuseaddr));
//3 bind()
int res = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (res == -1) {
perror("bind"), exit(-1);
}
printf("bind ok\n");
//4 listen()
listen(sockfd, 100);
while (1) {
//5 accept()
struct sockaddr_in from;
socklen_t len = sizeof(from);
int fd = accept(sockfd, (struct sockaddr *)&from, &len);
//会等待客户端的连接;//accept()是阻塞函数
if (fd == -1) {
perror("accept"), exit(-1);
}
pid_t pid = fork(); //支持多进程
if (pid == 0) {
printf("%s已连接\n", inet_ntoa(from.sin_addr));
//6 读写
char buf[100] = { }, buf1[100] = {
};
while (1) {
res = read(fd, buf, sizeof(buf));
if (res == -1) {
break;
}
printf("接收到内容:\nclient>%s\n", buf);
if (!strcmp(buf, "bye")) {
break;
}
strcpy(buf1, buf);
write(fd, buf1, strlen(buf1));
//write(fd,buf1,strlen(buf1)+1);//可以不用清空
memset(buf, 0, strlen(buf)); //清零
memset(buf1, 0, strlen(buf1)); //清零
}
//7 关闭两个描述符
close(fd); //关闭子进程fd
}
close(fd); //关闭父进程fd
printf("服务器 pid = %d\n", getpid());
}
//close(sockfd);
return 0;
}
/*
* TCP网络编程客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr = "";//不可以
addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
//IP地址为连接目标
//3连接
int res = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
char buf[100] = { };
char ret[100] = { };
while (1) {
printf("请输入聊天信息:>");
printf("如退出请输入bye,否则按enter键:\n>");
scanf("%s", buf);
scanf("%*[^\n]");
scanf("%*c");
write(sockfd, buf, sizeof(buf));
if (!strcmp(buf, "bye")) {
break;
}
printf("已发送%s\n", buf);
read(sockfd, ret, sizeof(ret));
printf("服务器返回内容:\n>>%s\n", ret);
memset(buf, 0, strlen(buf)); //清0
memset(ret, 0, strlen(ret));
};
//5关闭socket描述符
close(sockfd);
printf("客户端已退出\n");
return 0;
}
UDP的编程
UDP协议是无连接协议,不保持连接;因此UDP不保证数据的完整和正确,因为不会重发错误数据;
TCP占用资源大,但能保证数据的正确完整;
UDP占用资源小,但不保证数据的正确完整;
UDP虽然是无连接协议,但是也可以建立UDP连接;
QQ是基于UDP的,大部分网络程序也是基于UDP的;
SOCK_STREAM //数据流(用于TCP);卖自来水
SOCK_DGRAM //数据报(用于UDP);卖瓶装矿泉水
UDP不需要connect(),它使用不同的数据发送函数和接收函数;
sendto()//发送,相当于connect()和write()的结合
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
成功返回发送数据的大小,失败返回-1;
sendto(sockfd, buf1, sizeof(buf1), 0,
(struct sockaddr*)&from, sizeof(from));
recvfrom()//接收
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
成功返回接收到的数据大小,失败返回-1;
socklen_t len = sizeof(from);
recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr*)&from, &len);
//注意最后一个参数与sendto的最后一个参数类型不同
无连接的UDP发送数据时无法使用write(),而接收数据时如不需要保留发送方通信地址可以使用read(),如果向直到发送方是谁需要使用recvfrom();
/*
* UDP网络编程服务端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//IP地址为本服务器IP
//3绑定
int res = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
printf("通信已建立\n");
char buf[100] = { }, buf1[100] = { };
//int res1 = read(sockfd, buf, sizeof(buf));
//如果还要得到客户端的地址,需要用recvfrom();
struct sockaddr_in from;
socklen_t len = sizeof(from);
int res1 = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &len); //接收
strcpy(buf1, buf);
if (res1 == -1) {
perror("read"), exit(-1);
}
printf("已经读取%d字节,\n内容:%s\n", res1, buf);
sendto(sockfd, buf1, sizeof(buf1), 0, (struct sockaddr *)&from, sizeof(from)); //发送
printf("已返回%s\n", buf1);
//5关闭socket描述符
close(sockfd);
return 0;
}
/*
* UDP网络编程客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//IP地址为连接目标
//3连接//无连接的UDP不需要connect()
//int res = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
printf("请输入一个字符串\n");
char buf[100] = { }, buf1[100] = { };
scanf("%s", buf);
//write(sockfd,buf,sizeof(buf));//write()发送不了无连接的UDP
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&addr,
sizeof(addr));
//sendto()相当于connect()与write()的结合;
printf("已经写入\n");
read(sockfd, buf1, sizeof(buf1));
printf("服务器返回:%s\n", buf1);
//5关闭socket描述符
close(sockfd);
return 0;
}
使用UDP做时间服务器
/*
* UDP做时间服务器
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <time.h>
int sockfd;
void fa(int signo) {
printf("\nserver down...\n");
sleep(1);
printf("...\n");
close(sockfd);
exit(0);
}
int main() {
printf("请按Ctrl+C退出系统\n");
signal(SIGINT, fa);
printf("本服务器已启动pid = %d\n", getpid());
//1获取socket描述符
sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr.s_addr = inet_addr("172.17.5.3"); //取结构中的成员转换函数
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
printf("本服务器IP为%s\n", inet_ntoa(addr.sin_addr));
//IP地址为本服务器IP
//3绑定
int res = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
//if (res == -1) { perror("bind"), exit(-1); }
//4通信
printf("通信已建立...\n");
char buf[100] = { }, buf1[100] = { };
//int res1 = read(sockfd, buf, sizeof(buf));
struct sockaddr_in from;
socklen_t len = sizeof(from);
while (1) {
int res1 = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &len); //接收
if (res1 == -1) {
perror("read"), exit(-1);
}
strcpy(buf1, buf);
printf("%s已连接\n", inet_ntoa(from.sin_addr));
printf("已经读取%d字节:%s\n", res1, buf);
/* 取系统时间 */
time_t cur_time = time(0);
struct tm *cur = localtime(&cur_time);
//strcpy(buf1, ctime(&cur_time));//自动转换格式的时间
sprintf(buf1, "系统时间是%4d-%02d-%02d %02d:%02d:%02d",
cur->tm_year + 1900, cur->tm_mon + 1, cur->tm_mday,
cur->tm_hour, cur->tm_min, cur->tm_sec);
//变长的buf要清0,定长的buf不用清0;
sendto(sockfd, buf1, strlen(buf1), 0, (struct sockaddr *)&from, sizeof(from)); //发送
printf("已返回%s\n", buf1);
}
//5关闭socket描述符
//close(sockfd);
return 0;
}
/*
* UDP时间服务客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
//1获取socket描述符
int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"), exit(-1);
}
//2准备通信地址
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(2222); //host to network hostshort
//addr.sin_addr = "";//不可以
char ip_addr[20] = { };
printf("请输入一个IP地址:\n>>");
scanf("%s", ip_addr);
//addr.sin_addr.s_addr = inet_addr("172.17.5.3");
addr.sin_addr.s_addr = inet_addr(ip_addr); //取结构中的成员转换函数
//IP地址为连接目标
//3连接//无连接的UDP不需要connect()
//int res = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
//if(res == -1){perror("bind"),exit(-1);}
//4通信
printf("请输入一个字符串\n");
char buf[100] = { }, buf1[100] = {
};
scanf("%s", buf);
//write(sockfd,buf,sizeof(buf));//发送不了无连接的UDP
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&addr,
sizeof(addr));
//sendto()相当于connect()与write()的结合;
printf("已经写入\n");
read(sockfd, buf1, sizeof(buf1));
printf("服务器返回:>>>>>>%s\n", buf1);
//5关闭socket描述符
close(sockfd);
return 0;
}
从sockaddr中取得Ip地址和端口号
在socket编程中,服务器端accept()等待一个客户端的连接,当连接成功后,accept拷贝客户端的地址信息到sin_addr里面,我们如何从sin_addr取得此客户端的Ip地址和端口号呢?
实际上,当sockaddr_in.sin_family = AF_INET时,sockaddr = sockaddr_in;
据此,我们可以做一下转换,就可以利用inet_ntoa()来得到ip地址和端口号:
int new_fd = accept(sock, &clientAddr, &sin_size);
if (new_fd < 0) {
printf("accept error\n");
} else {
//将sockaddr强制转换为sockaddr_in
sockaddr_in sin;
memncpy(&sin, &clientAddr, sizoef(sin));
//取得ip和端口号
sprintf(info.ip, inet_ntoa(sin.sin_addr));
info.port = sin.sin_port;
info.sock = new_fd;
}