目录
一、概述
1、TCP/UDP对比
1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接;
2.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付;
3. TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等);
4. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信;
5. TCP首部开销20字节;UDP的首部开销小,只有8个字节;
6. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
2、端口号作用
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。
因为IP 地址与网络服务的关系是一对多的关系。 实际上是通过“IP地址+端口号”来区 分不同的服务的。 端口提供了一种访问通道, 服务器一般都是通过知名端口号来识别的。
例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
二、字节序
1、字节序是什么
字节序是计算机存储多字节数据的方式,目前主流的方式有:大端字节序和小端字节序,字节序主要是针对多字节的数据类型,比如 short、int
等类型
- 大端字节序(Big endian)
高位字节存储在内存的低地址上,低位字节存储在内存的高位地址上
- 小端字节序(Little endian)
高位字节存储在内存的高地址上,低位字节存储在内存的低地址上
网络字节序 = 大端字节序
2、如何理解字节序
比如:对于 0x01020304,它的大端和小端字节序在内存中的布局如下图所示
0x 01 02 03 04 总共四个字节大小,以人们习惯的阅读顺序,0x01 处于左边,属于高位字节,0x04 处于右边,属于低位字节;内存地址从 0x 00 00 00 07 到 0x 00 00 00 0A 4个字节的空间,刚好能存储得下。
根据大端字节序的的规则:高位字节存储在内存低地址,所以处于高位字节的 0x01 存储在 0x 00 00 00 07 地址处,紧接着 次高位字节 0x02 存储在次低地址 0x 00 00 00 08 处,剩下的两个字节 0x03 和 0x04 分别存储于 0x 00 00 00 09 和 0x 00 00 00 0A 地址处,最终以 0x 01 02 03 04 存储。
小端字节序和大端刚好相反,它指的是 高位字节存储在内存高地址处,所以处于高位字节的 0x01 存储在 0x 00 00 00 0A 地址处,次高位字节 0x02 存储在次高地址 0x 00 00 00 09 处,余下的 0x03 和 0x04 分别存储于 0x 00 00 00 08 和 0x 00 00 00 07 地址处,最终以 0x 04 03 02 01 存储。
3、字节序转换api
#include <netinet/in.h>
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指定地址让操作系统自己获取
三、socket编程
1、流程
服务器实现步骤:
- 使用socket()函数创建套接字
- 为套接字添加信息(IP地址和端口号)
- listen()设置套接字为监听模式,使服务器进入被动打开状态
- 接收客户端的连接请求
- 接收、应答客户端的数据请求
- 关闭套接字,终止连接
客户端实现步骤
- 使用socket()函数创建套接字
- 调用connect()函数建立一个与TCP服务器的连接
- 发送数据请求,接收服务器的数据应答
- 终止连接
2、socket函数
-
socket()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//功能:用于绑定IP地址和端口号到socketfd
//参数:
//sockfd:是一个socket描述符
//addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构这个地址结构根据地址创建socket 时的地址协议族的不同而不同
-
listen()
int listen(int sockfd, int backlog);
//参数:
//sockfd:sockfd是socket系统调用返回的服务器端socket描述符
//backlog:backlog指定在请求队列中允许的最大请求数
-
accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
数据收发
-
地址转换API
int inet_aton(const char* straddr,struct in_addr *addrp);
//把字符串形式的“192.168.1.123”转为网络能识别的格式
char* inet_ntoa(struct in_addr inaddr);
//把网络格式的ip地址转为字符串形式
3、socket服务端代码实现(无连接客户端)
server1.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main()
{
int s_fd;
//1.socket
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socked");
exit(-1);
}
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET; //TPv4因特网域
s_addr.sin_port = htons(8989);
inet_aton("192.168.47.128",&s_addr.sin_addr); //虚拟机linux的ip地址
//2.bind
bind(s_fd, (struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
//4.accept
int c_fd = accept(s_fd,NULL,NULL);
//5.read
//6.write
printf("connect\n");
while(1);
return 0;
}
4、socket服务端/客户端
server2.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int s_fd;
int n_read;
char readBuf[128];//定义一个数组
char *msg = "I got your message";
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("socked");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8989);
inet_aton("192.168.47.128",&s_addr.sin_addr);
//2.bind
bind(s_fd, (struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
//4.accept
int clen = sizeof(struct sockaddr_in);//长度
int c_fd = accept(s_fd,(struct sockaddr *)&c_addr, &clen);
if(c_fd == -1){
perror("accept");
}
//成功则打印客户端ip地址
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr)); //inet_ntoa()函数,把网络格式的 IP 地址转为字符串形式
//==================服务端与客户端之间数据交互=============================
//5.read
n_read = read(c_fd,readBuf,128);//把客户端的内容读到readBuf
if(n_read == -1){
perror("read");
}else{
printf("get message: %d,%s\n",n_read,readBuf);//打印readBuf中内容
}
//6.write
write(c_fd,msg,strlen(msg));//写入操作
//======================================================================
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int c_fd;
int n_read;
char readBuf[128];
char *msg = "msg from client";
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
perror("socked");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8989);
inet_aton("192.168.47.128",&c_addr.sin_addr);
//2.connect
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in)) == -1){
perror("connect");
exit(-1); //连接不到服务端会阻塞,出错直接跳出程序,以免搞崩代码
}
//3.send
write(c_fd,msg,strlen(msg));
//==================读取服务端的信息==================
//4.read
n_read = read(c_fd,readBuf,128);
if(n_read == -1){
perror("read");
}else{
printf("get message from server: %d ,%s\n",n_read,readBuf);
}
//===================================================
return 0;
}
四、实现双方聊天
BUG:多个客户端接入时,服务端发送的数据,不知道哪个客户端会收到。
原因:在光标输入时,不知道是哪个子进程在运行gets()。可以设置服务端自动回复。
服务端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
int n_write;
int mark=0;
char readBuf[128];
char returnMsg[128]={0};
struct sockaddr_in c_addr;
struct sockaddr_in s_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
memset(&s_addr,0,sizeof(struct sockaddr_in));//数据清空
if(argc != 3){
printf("参数出错\n");
exit(-1);
}
//1.socket
s_fd=socket(AF_INET,SOCK_STREAM,0); //ipv4 tcp协议
if(s_fd == -1){
printf("创建socket失败");
perror("socket:");
exit(-1);
}
s_addr.sin_family = AF_INET;//ipv4
s_addr.sin_port = htons(atoi(argv[2]));//端口号,选择5000以上。honts返回网络字节序,atoi(argv[2])防止端口被占用
inet_aton(argv[1],&s_addr.sin_addr);//转换为网络能识别的格式
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);//监听10个连接
//4.accept
int clen = sizeof(struct sockaddr_in);
while(1){//不断接收客户端
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
printf("连接失败\n");
perror("accept:");
exit(-1);
}
printf("客户端的ip:%s\n",inet_ntoa(c_addr.sin_addr)); //把网络格式的ip地址打印成字符串格式
mark++;
if(fork() == 0){
if(fork() == 0){
while(1){
//sprintf(returnMsg,"欢迎第%d号客户端",mark);
memset(returnMsg,0,sizeof(returnMsg));
printf("请输入:\n");
gets(returnMsg);
n_write=write(c_fd,returnMsg,strlen(returnMsg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read=read(c_fd,readBuf,128);
if(n_read == -1){
perror("read:");
}else{
printf("客户端(%d)-->:%s\n",mark,readBuf);
}
}
}
}
return 0;
}
客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int c_fd;
int n_read;
int n_write;
int c_connect;
char readBuf[128];
char returnMsg[128]={0};
char *quit="quit";
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));//数据清空
if(argc != 3){
printf("参数出错\n");
exit(-1);
}
//1.socket int socket(int domain, int type, int protocol);
c_fd=socket(AF_INET,SOCK_STREAM,0);
//ipv4 tcp协议
if(c_fd == -1){
printf("创建socket失败");
perror("socket:");
exit(-1);
}
c_addr.sin_family=AF_INET;//ipv4
c_addr.sin_port=htons(atoi(argv[2]));//端口号,选择5000以上。honts返回网络字节序
inet_aton(argv[1],&c_addr.sin_addr);//转换为网络能识别的格式
//2.connect int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
c_connect=connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr));
if(c_connect == -1){
printf("连接失败\n");
perror("connect:");
}
while(1){
if(fork() == 0){
while(1){//不断写入
memset(returnMsg,0,sizeof(returnMsg));
printf("请输入:\n");
gets(returnMsg);
//3.write/send ssize_t write(int fd, const void *buf, size_t count);
n_write=write(c_fd,returnMsg,strlen(returnMsg));
if(strcmp(quit,returnMsg) == 0){//如果输入quit则客户端就退出
exit(0);
}
}
}
while(1){//不断读取
memset(readBuf,0,sizeof(readBuf));//不断清空数据防止数据重复出现
//4.read ssize_t read(int fd, void *buf, size_t count);
n_read=read(c_fd,readBuf,128);
if(n_read == -1){
perror("read:");
}else{
printf("服务端-->:%s\n",readBuf);
}
}
//5.close int close(int fd);
close(c_fd);
}
return 0;
}
五、服务端自动回复
修改服务端部分:
while(1){//不断接收客户端
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
printf("连接失败\n");
perror("accept:");
exit(-1);
}
printf("客户端的ip:%s\n",inet_ntoa(c_addr.sin_addr)); //把网络格式的ip地址打印成字符串格式
mark++;
if(fork() == 0){
if(fork() == 0){
while(1){
sprintf(returnMsg,"客户端%d连接正常",mark);
n_write=write(c_fd,returnMsg,strlen(returnMsg));
sleep(5);
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read=read(c_fd,readBuf,128);
if(n_read == -1){
perror("read:");
}else{
printf("客户端(%d)-->:%s\n",mark,readBuf);
}
}
}
}