目录
一、网络编程概述
网络通信手段的一种,我们在之前学了进程间通信,包括管道、消息队列、共享内存、信号和信号量,这些通信方式有一个共同的特点,就是他们都是在依赖Linux内核在单机上进行进程的通信,而面对多机之间的通信,这些手段就远远不够了。所以我们引入网络,利用网络来实现多机之间的通信。
多机通信有好多例子,比如Linux服务器和Adroid、 IOS、C51、x86的Linux之间的通信。 既然谈到网络编程,就一定需要地址,包括IP地址和端口号。IP地址用来标识一台设备,端口号用来标识一台设备上的进程。除了这个,还需要协议来作为支撑,包括TCP、UDP和HTTP协议等,协议作为一种规则,约定了通信双方的数据格式,就好比两个人必须都用中文才能听懂对方说话。
我们在这里主要用到的是Socket网络编程,也叫套接字网络编程,主要用到TCP和UDP协议。
1. TCP/UDP对比:(面试)
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的。UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)。
- 每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的交互通信。
- TCP首部开销20字节;UDP的首部开销小,只有8个字节。
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
2. 端口号的作用:(面试)
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等。
这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。
实际上是通过“IP地址+端口号”来区 分不同的服务的。端口提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。3000以下的端口号一般是操作系统所使用的,用户一般使用5000到9000的端口号。
二、字节序(面试)
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
字节序分为小端字节序和大端字节序:
- Little endian:将低序字节存储在起始地址
- Big endian:将高序字节存储在起始地址
例子:在内存中双字0x01020304(DWORD)的存储方式
内存地址:4000&4001&4002&4003
如上图,地址从下至上为依次递增,小端字节序一般是将数据低位从低地址开始依次向上存储的一种方式。
如上图,地址从下至上为依次递增,大端字节序一般是将数据高位放在低地址开始依次向上存储的一种方式。
x86系列CPU都是little-endian的字节序.
注意,网络字节序指的是大端字节序。
三、Socket网络编程步骤
关于网络编程,我们可以打一个比喻:
具体步骤可以总结为以下图片:
总结步骤:
- 创建套接字
- 为套接字添加信息(IP地址和端口号)
- 监听网络连接
- 监听到有客户端接入,接受一个连接
- 数据交互
- 关闭套接字,断开连接
四、Linux提供的API简析
1. 指定讲“汉语”(连接协议)
int socket(int domain, int type, int protocol);
2. 地址准备好
3. 地址转换API
把字符串形式的“192.168.1.123”转为网络能识别的格式:
int inet_aton(const char* straddr,struct in_addr *addrp);
网络格式的ip地址转为字符串形式:
char* inet_ntoa(struct in_addr inaddr);
4. 监听
5. 数据收发
第一套:
第二套:
6. 客户端的connect函数
7. 字节序转换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服务端客户端代码实现
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int s_fd;
//1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8989);
inet_aton("192.168.245.139",&s_addr.sin_addr);
//3.listen
listen(s_fd, 10);
//4.accept
int c_fd = accept(s_fd, NULL, NULL);
//5.read
//6.write
printf("connected\n");
while(1);
return 0;
}
服务端连接测试:
运行代码:
调用windows命令行连接:
运行结果:
以上测试证明,我们写的服务端的代码是可以跑的通的。
数据收发代码测试:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main()
{
int s_fd;
int n_read;
char readBuf[128];
char *msg = "I get your message";
struct sockaddr_in c_addr;
struct sockaddr_in s_addr;
//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("192.168.245.139",&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");
}
printf("get connect: %s\n",inet_ntoa(s_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));
printf("connected\n");
while(1);
return 0;
}
运行结果:
windows命令行下连接服务端:
客户端代码:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main()
{
int c_fd;
int n_read;
char readBuf[128];
char *msg = "message from client";
struct sockaddr_in c_addr;
//1. socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8989);
inet_aton("192.168.245.139",&c_addr.sin_addr);
//2.connect
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(-1);
}
//3.write
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:%d,%s\n",n_read,readBuf);
}
return 0;
}
测试结果:
六、Socket实现双方聊天
服务端代码:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc,char **argv)
{
int s_fd;
int n_read;
int c_fd;
char readBuf[128];
char msg[128] = {0};
struct sockaddr_in c_addr;
struct sockaddr_in s_addr;
if(argc != 3){
printf("param is not good\n");
}
//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);
//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);
while(1){
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if(c_fd == -1){
perror("accept");
}
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){
if(fork() == 0){
while(1){
memset(msg,0,sizeof(msg));
printf("input:");
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);
}
}
}
}
return 0;
}
客户端代码:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc,char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
char msg[128];
struct sockaddr_in c_addr;
if(argc != 3){
printf("param is not good\n");
}
//1. socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//2.connect
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(-1);
}
while(1){
if(fork() == 0){
while(1){
memset(msg,0,sizeof(msg));
printf("input: ");
gets(msg);
//3.write
write(c_fd, msg, strlen(msg));
}
}
while(1){
//4.read
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);
}
}
}
return 0;
}
运行结果:
七、Socket多方消息收发
在上面,我们实现了服务端和客户端双方的聊天,但是我们遇到一个问题,就是当我们多开一个客户端的时候,客户端可以给服务端发消息,但是服务端给客户端发消息的时候,就会产生不确定的情况。就是当有多个客户端访问服务端的时候,服务端不知道把消息发送给谁,其无法解决精准的一对一的通信问题,经过一番研究发现,其实不是因为服务端无法对接客户端进程的问题,而是因为服务端输入发送内容时的光标无法解析发给谁的问题,下面对服务端代码做了一些改进,验证了能够实现服务端和客户端能够一对一精准通信的问题。
服务端代码:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc,char **argv)
{
int s_fd;
int n_read;
int c_fd;
char readBuf[128];
char msg[128] = {0};
int mark =0;
struct sockaddr_in c_addr;
struct sockaddr_in s_addr;
if(argc != 3){
printf("param is not good\n");
}
//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);
//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);
while(1){
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if(c_fd == -1){
perror("accept");
}
mark++;
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){
if(fork() == 0){
while(1){
sprintf(msg,"Welcome No.%d client\n",mark);
write(c_fd, msg, strlen(msg));
sleep(3);
}
}
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);
}
}
}
}
return 0;
}
客户端代码同实现双方聊天的代码。
运行结果:
通过以上代码验证,我们验证了多个客户端和服务端通信的可能性,但是要是实现服务端键盘输入给指定客户端发消息,这需要涉及很多算法方面的内容,而且比较复杂。这里我们预想可以把客户端作为一个中转服务器,最终实现多个客户端之间信息的保存,互相之间的通信问题,这里我们等待后续再探。
本次关于网络编程的学习分享就到这里,如有不当,敬请批评指正。
为了梦想,一路狂奔。