Linux系统编程之网络编程篇
网络编程概述
前面一章学习了进程间通信,进程间通信的方法包括管道、消息队列、共享内存、信号及信号量等,这些方式都有一个特点,那就是依赖于Linux内核。也就是说,这些进程间的通信手段仅适用于本机通讯,无法实现多机通信。
如果要实现多机之间的通信,就需要使用网络通信。不同主机以主机的网络地址为依据进行主机间的通讯。一台主机的网络地址由两部分组成:IP地址+端口号。由于一台主机可能运行多个服务,端口号的作用,就是区分主机上的不同服务(类比于医院的不同科室/不同窗口)。找到地址后,与服务器间的通信实际就是与特定地址的进程之间的数据交换。在进行数据交换时,除了注意地址还要注意协议的问题。网络通信中的协议,实际就是对数据格式的约定,规定了双方发送什么样的数据。
本教程网络通信主要是SOCKET(套接字)网络编程,涉及到的协议主要是TCP与UDP,这两个协议之间的区别,总结为如下几点:
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前 不需 要建立连接
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) - 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
字节序
学习网络编程之前,先学习一下字节序。字节序指的是多字节数据在计算机内存中存储或在网络传输时各字节的存储顺序。常见的字节序有两种:小端序(Little endian)和大端序(Big endian)
以在内存地址4000&4001&4002&4003
存放双字0x01020304
(DWORD)为例,如果是小端序,即低序字节存储在起始位置,则数序为01 02 03 04
;反之若为大端序,即高序字节存储在起始位置,则为04 03 02 01
x86系列的CPU,都是小端序,而网络字节序默认都是大端序
如果要转换字节序,有如下的函数可供使用:
1 |
|
Socket编程步骤
以服务的提供方,也就是服务端为例,使用Socket进行网络编程,大致可以总结归纳为如下几个步骤:
- 创建套接字,这一步的功能相当于使用open函数打开文件,获取的是网络描述符,后续操作均以描述符为依据
- 为套接字添加信息,主要是IP地址和端口号
- 监听网络连接,具体使用listen对特定的端口号进行监听
- 监听到有客户端接入,接收连接
- 与客户端进行数据交互
- 关闭套接字,断开连接

LinuxSocket相关API介绍
下面,依照一小节介绍的Socket通信步骤,依次简析Linux中可用的相关API。
- 创建套接字句柄:
socket()
函数。函数原型:其中,三个参数分别有如下可选值:1
2
3#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocal)
- domain,指定使用的协议族,通常取值
AF_INET
AF_INET
,IPV4
因特网域AF_INET6
,IPV6
因特网域AF_UNIX
,UNIX
因特网域AF_ROUTE
,路由套接字AF_KEY
,密钥套接字AF_UNSPEC
,未指定
- type,指定
Socket
的类型- SOCK_STREAM:
流式套接字,提供可靠的面向连接的通信流,使用TCP
协议,从而保证了数据传输的正确性和顺序性 - SOCK_DGRAM:
数据报套接字,定义一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,不保证可靠和无差错,使用数据报协议UDP
- SOCK_RAW:
允许使用低层协议,原始套接字允许对低层协议如IP
和ICMP
进行直接访问,功能强但但不便使用,主要用于协议的开发
- SOCK_STREAM:
- protocol,指定具体使用哪一个协议。这一项一般会根据前两个参数进行默认选择,所以通常赋值
0
0
选择type
类型对应的默认协议IPPROTO_TCP
TCP传输协议IPPROTO_UDP
UDP传输协议IPPROTO_SCTP
SCTP传输协议IPPROTO_TIPC
TIPC传输协议
- 设置IP地址和端口:
bind()
函数,原型:该函数参数说明如下:1
2
3#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
- sockfd,为前面通过socket函数创建得到的socket描述符
- addr,是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,此地址结构根据地址创建socket时的地址协议族不同而不同,不是简简单单的“IP:port”字符串就能表达的。因此,在使用时通常要对其进行转换,才能进行操作
关于sockaddr的结构:1
2
3
4
5
6
7
8
9
10struct sockaddr{
unisgned shortas_family;//协议族char
sa_data[14];//IP+端口
};
struct sockaddr_in {
sa_family_tsin_family;//产协议族
in_port_tsin_port;//端口号
struct in_addr sin_addr;//IP地址结构体/
unsigned char sin_zero[8];//填充 没有实际意义。只是为跟sockaddr结构在内存中对齐这样两者才能相互转换*
};
关于sockaddr与IP地址的转换api:1
2
3
4
5#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, strcut in_addr *inp);//将字符串形式的IP地址+端口转换为网络能识别的格式
char *inet_ntoa(struct in_addr in)//将网络格式的IP地址转换为字符串形式
端口监听:
listen()
函数,功能有三:监听、设置能处理的最大连接数、维护两个队列(未完成连接队列,状态SYS_REVD
和已完成连接队列,状态ESTABLISHED
)
函数原型:1
2
3#include <sys/socket.h>
#include <sys/types.h>
int listen(int sockfd, int backlog)其中,sockfd为socket描述符,backlog制定了在请求队列中允许的最大请求数,通常以成功的三次握手为单位
连接建立:
connect()/accept()
函数。函数功能:connect()
由客户端调用,以请求连接。accept()
由TCP服务端调用,用于返回未完成连接队列的对头以完成连接,如果未完成连接队列为空,那么进程进入睡眠状态
原型:1
2
3#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)数据收发,有两套api:
- 字节流读取函数
read()/write()
,它们所输入或输出的字节数可能比请求的少,这一点易于常规的I/O读取函数 - TCP上套接字数据收发专用api
send()/recv()
,函数原型:1
2
3
4
5
6
7
8#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf,size_t len, int flags)
//三参数分别为套接字、待发数据和数据长度,只能用于处于连接状态的套接字
//参数s为accept函数成功的返回值
//msg指向待发送数据的缓冲区
//len为待发送数据的长度,flags为控制选项一般设0
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
Socket服务端代码实现
相关的api,上一小节基本都介绍过了,这一节主要编程实现(一个小技巧:对于一些函数的参数不知道其原型是什么,比如struct sockaddr_in
这个结构体的内容,可以进入/usr/include
命令,使用grep "要查找的数据定义字符串" * -nir
,会自动将系统中所有头文件涉及到参数的信息打印出来)

服务端代码的编写,主要还是按照前面介绍过的几个步骤:先创建socket描述符,设置信息、监听、建立连接、数据的收发,最后关闭描述符。在编写的过程中遇到过的问题有:
- 引用头文件,
<linux/in.h>
和<netinet/in.h>
包含一个就可,否则会报错重定义(这两个都定义了sockaddr_in) inet_aton(argv[1],&s_addr.sin_addr)
,注意第二个参数是指针- listen函数限额的问题,第二个参数代表的是未完成连接队列和已完成连接队列容量之和
- write函数,建议先写好一个字符串变量,传入指针,否则容易乱码
代码:
1 |
|

Socket客户端代码实现
与服务端相比,客户端在通信中的地位是服务的接受则会,因此工作内容少得多,在代码实现上也更简单。具体来说,客户端实现的流程是创建一个socket描述符,设置客户端的地址信息,然后调用connect连接函数建立连接,进行数据交互。与服务端的步骤相比,没有bind、listen与accept
1 |
|
运行效果:

双方聊天实现
在上一小节应用的基础上,优化相关逻辑,实现服务端和客户端之间不间断的数据通信。之前讲进程的时候,提到如果服务器需要创建新进程来与客户端进行交互,可以使用fork()创建新进程。这一节客户端与服务端的消息交互,就通过fork创建的新进程进行控制。
客户端代码调整如下,主要变动:
- msg采用字符数组存储,每次读取之前先用memset清空缓存以防消息的重复
- fork()创建子进程继续宁消息交互
- while(1)循环控制连接不关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
char msg[128]={0};
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
//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);
}
printf("connected ...\n");
//int mark = 0;
while(1){
if(fork()==0){//子进程,一直输入
while(1){
memset(msg,0,sizeof(char)*128);
printf("%d, your input: \n",getpid());
gets(msg);
write(c_fd,msg,strlen(msg));
}
}
while(1){//主进程,从服务器取消息
memset(readBuf,0,sizeof(char)*128);
n_read=read(c_fd,readBuf,128);
if(n_read == -1){
perror("read");
} else {
printf("%d, msg from server: %s\n",getpid(),readBuf);
服务端主要变动:同客户端
1 |
|