Linux系统编程—socket网络编程
- 理论概念
- 1. TCP与UDP对比
- 端口号作用
- socket开发过程
- 服务端
- 1. socket 创建套接字
- 2. bind 绑定IP+端口
- 3. listen 监听客户端
- 4. accept 接收客户端
- 5. read / write 数据传输
- 客户端
- 1. socket 创建套接字
- 2. connect 连接服务
- 3. read / write 数据传输
- server.c与client.c程序案例
理论概念
1. TCP与UDP对比
-
TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需 要建立连接
-
TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
-
TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) -
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
-
TCP首部开销20字节;UDP的首部开销小,只有8个字节
-
TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
端口号作用
-
一台拥有IP地址的主机可以提供许多服
,比如Web服务、FTP服务、SMTP服务等 -
通过“IP地址+端口号”来区 分不同的服务,端口提供了一种访问通道,服务器一般都是通过端口号来识别的。
socket开发过程
服务端
1. socket 创建套接字
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值: 成功返回网络标识符,失败返回 -1
参数分析
domain: 指明所使用的协议族,通常为AF_INET
,表示互联网协议族(TCP/IP协议族):
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 进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
protocol:
通常赋值 0
0
选择 type 类型对应的默认协议IPPROTO_TCP
TCP 传输协议IPPROTO_UDP
UDP 传输协议IPPROTO_SCTP
SCTP 传输协议IPPROTO_TIPC
TIPC 传输协议
2. bind 绑定IP+端口
- 功能:用于绑定IP地址和端口号到sockfd
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//成功返回0,失败返回-1
参数分析
参数 | 类型/值 |
---|---|
sockfd | 服务端网络标识符,即 socket 的返回值 |
struct sockaddr *addr | 绑定服务器端 网络协议、IP 地址和端口号的地址结构指针 |
socklen_t addrlen | 结构体大小 |
- addr
一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针,指向要绑定给 sockfd 的协议地址结构,这个地址结构根据地址创建socket 时的地址协议族的不同而不同
注:
IPV4的实际结构体如下
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
同等替换为:
struct sockaddr_in {
sa_family_t sin_family; //协议族
in_port_t sin_port; //端口号
struct in_addr sin_port; //IP地址结构体
unsigned char sin_zero[8]; //填充 无实际意义
};
struct sockaddr_in
结构体已在Linux内核系统中定义,成员参数配置如下;
-
sa_family_t
sin_family
协议族, TCP协议族,AF_INET
-
in_port_t sin_port
端口号, 类型为网络字节序,需利用htons函数进行转化为网络字节数
例:s_addr.sin_port = htons(8888);
-
struct in_addr sin_port
IP地址结构体,struct in_addr 类型结构体
系统中定义的struct in_addr 结构体struct in_addr { __be32 s_addr; };
将IP地址转化为网络能识别的格式函数原型:
#include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char* straddr,struct in_addr *addrp);
转化典例:
inet_aton("127.0.0.1", &s_addr.sin_addr );
地址转化API
int inet_aton(char *straddr,struct in_addr *addr) //将字符串形式的"127.0.0.1"转化为网络能识别的格式 char *inet_ntoa(struct in_addr inaddr); //把网络格式的ip地址转化为字符串形式
结构体配置及bin函数整合实例
struct sockaddr_in s_addr; //结构体变量定义
s_addr.sin_family = AF_INET; //配置网络协议族,TCP协议
inet_aton("127.0.0.1", &s_addr.sin_addr ); //配置结构体成员IP地址
s_addr.sin_port = htons(8888); //配置结构体成员端口号
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in)); //绑定IP+端口号
3. listen 监听客户端
函数原型
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数分析
参数 | 类型/值 |
---|---|
sockfd | 服务端网络标识符 |
backlog | 允许的最大客户单请求个数 |
功能
- 设置能处理的最大连接数,listen()并未开始接受连线,只是设置 socket 的 listen 模式,listen 函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
- 内核为任何一个给定监听套接字维护两个队列:
- 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于SYN REVD 状态
- 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED 状态
4. accept 接收客户端
函数原型
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值: 成功返回接入的客户端网络标识符,失败返回-1
参数分析
参数 | 类型/值 |
---|---|
sockfd | 服务端网络标识符 |
struct sockaddr *addr | struct sockaddr 结构体类型地址,用来返回已连接的对端(客户端)的协议地址 |
socklen_t *addrlen | 客户端地址长度,但为地址,需先取长度,后取地址 |
例:
addrlen = sizeof(struct sockaddr_in);
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &addrlen);
5. read / write 数据传输
#include <unistd.h>
ssize_t read(int c_fd, void *buf, size_t count);
ssize_t write(int c_fd, const void *buf, size_t count);
- 函数使用方法同Linux文件编程用法,参数主要为:网络标识符、读写Buf以及读写字节数
- 详情请参考博文:Linux系统编程—文件API编程
客户端
1. socket 创建套接字
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值: 成功返回网络标识符,失败返回 -1
参数分析
- domain: 指明所使用的协议族,通常为
AF_INET
,表示互联网协议族(TCP/IP协议族): - type: 指定socket类型,
SOCK_STREAM
流式套接字提供可靠的、面向连接的通信流,它使用TCP
协议, - protocol: 通常赋值
0
,选择 type 类型对应的默认协议
2. connect 连接服务
- 功能:该函数用于绑定之后的client 端(客户端),与服务器建立连接
函数原型
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//成功返回0,失败返回-1
参数分析
参数 | 类型/值 |
---|---|
sockfd | 服务端网络标识符 |
struct sockaddr *addr | 服务器端的 IP 地址和端口号的地址结构指针 |
socklen_t addrlen | 地址长度常被设置为 sizeof(struct sockaddr_in) |
结构体实际配置同服务端,案例如下
struct sockaddr_in c_addr; //定义结构体变量
c_addr.sin_family = AF_INET; //配置成员网络协议族
inet_aton("127.0.0.1", &c_addr.sin_addr ); //配置结构体成员IP地址
c_addr.sin_port = htons(8888); //配置结构体成员端口号
connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr_in)); //绑定IP+端口号
3. read / write 数据传输
#include <unistd.h>
ssize_t read(int c_fd, void *buf, size_t count);
ssize_t write(int c_fd, const void *buf, size_t count);
- 函数使用方法同Linux文件编程用法,参数主要为:网络标识符、读写Buf以及读写字节数
server.c与client.c程序案例
server.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int s_fd, c_fd; //定义网络标识符
int nread; //读取字节数
char readBuf[128]={0}; //读取缓存
char msg[]="Return from server: 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){
printf("socket error!\n");
exit(-1);
}
//2.bind 绑定IP+端口号
s_addr.sin_family = AF_INET; //配置网络协议
inet_aton("127.0.0.1", &s_addr.sin_addr ); //配置IP地址
s_addr.sin_port = htons(8888); //配置端口号
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in)); //绑定IP+端口号
//3.linsten 监听客户端接入
listen(s_fd, 10);
printf("listing......\n");
//4.accept 接收客户端接入
int addrlen = sizeof(struct sockaddr_in);
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &addrlen);
if(c_fd == -1){
printf("c_fd error!\n");
exit(-1);
}
else
printf("connect client: %s\n",inet_ntoa(c_addr.sin_addr));
//5.read
memset(readBuf, '\0', 128);
nread = read(c_fd, readBuf, 128); //从客户端读取128字节数据到readBuf缓存
if(nread == -1)
{
printf("nread error\n");
exit(-1);
}
printf("Receive: %d Byte context:%s\n",nread, readBuf);
//6.write
write(c_fd, msg, strlen(msg)); //应答客户端
memset(msg,'\0',128);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int c_fd;
int nread;
int addrlen;
char readBuf[128]={0};
char msg[]="hello world";
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){
printf("socket error!\n");
exit(-1);
}
//2. connect 与服务器建立连接
c_addr.sin_family = AF_INET;
inet_aton("127.0.0.1", &c_addr.sin_addr );
c_addr.sin_port = htons(8888);
if( connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr_in)) == -1)
{
printf("connect error\n");
exit(-1);
}
//3.write
write(c_fd, msg, strlen(msg));
memset(msg, '\0', 128);
//4.read
memset(readBuf,'\0',128);
nread = read(c_fd, readBuf, 128);
if(nread == -1){
printf("nread error\n");
exit(-1);
}
printf("read from server: %d Byte context:\n%s\n",nread, readBuf);
return 0;
}
试问:如何实现双方自由聊天
- 服务器通过创建子进程用于和每个不同的客户端进行数据交互,父进程负责接收后续接入的客户端
- 子进程内部再次创建子进程用来发送数据,父进程则实时接收来自服务器的数据
server.c 核心代码
//4.accept
while(1)
{
//父进程用于接收其他接入的客户端
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &addrlen);
if(c_fd == -1){
printf("c_fd error!\n");
exit(-1);
}
else
printf("connect client: %s\n",inet_ntoa(c_addr.sin_addr));
cnt++;
if(fork() == 0) //创建进程,子进程用于和客户端进行数据交互
{
if(fork() == 0) //子进程用于向客户端发送数据
{
//write
while(1)
{
printf("Inputs: ");
scanf("%s",msg);
write(c_fd, msg, strlen(msg));
memset(msg,'\0',128);
}
}
else //父进程用于接收来自客户端发送的数据
{
//read
while(1)
{
memset(readBuf, '\0', 128);
nread = read(c_fd, readBuf, 128);
if(nread == -1)
{
printf("nread error\n");
exit(-1);
}
printf("Receive: %d Byte context:%s\n",nread, readBuf);
}
}
}
}
client.c 核心代码
while(1)
{
if(fork() == 0) //子进程用于向服务器发送数据
{
while(1) //write
{
printf("Input: ");
scanf("%s",msg);
//gets(msg);
write(c_fd, msg, strlen(msg));
memset(msg, '\0', 128);
}
}
else
{ //父进程用于接收服务器发送的数据
while(1) //read
{
memset(readBuf,'\0',128);
nread = read(c_fd, readBuf, 128);
if(nread == -1){
printf("nread error\n");
exit(-1);
}
printf("read from server: %d Byte context:\n",nread);
printf("%s\n",readBuf);
}
}