tips
net/ipv4/af_inet.c 查看:inet_stream_ops 和 inet_dgram_ops
TCP和UDP
TCP(传输控制协议)和UDP(用户数据报协议)是网络体系结构TCP/IP模型中传输层一层中的两个不同的通信协议。
- TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流,TCP套接口是字节流套接口(stream socket)的一种。
- UDP:用户数据报协议。UDP是一种无连接协议。UDP套接口是数据报套接口(datagram socket)的一种。
TCP Client-Server框架
服务器程序流程:
- 程序初始化
- 填写本机地址信息
- 绑定并监听一个固定的端口
- 收到Client的连接后建立一个socket连接
- 产生一个新的进程与Client进行通信和信息处理
- 子通信结束后中断与Client的连接
客户端程序流程:
- 程序初始化
- 填写服务器地址信息
- 连接服务器
- 与服务器通信和信息处理
- 通信结束后断开连接
UDP Client-Server框架
服务器程序流程:
程序初始化
填写本机地址信息
绑定一个固定的端口
收到Client的数据报后进行处理与通信
通信结束后断开连接
客户端程序流程:
程序初始化
填写服务器地址信息
与服务器通信和信息处理
通信结束后断开连接
Socket编程
Socket接口是TCP/IP网络的API,
网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。
- 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用
- 数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用
创建套接字
int socket(int domain, int type, int protocol);
建立地址和套接字的联系
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
服务器端侦听客户端的请求
int listen(int sockfd, int backlog);
建立服务器/客户端的连接 (面向连接TCP)
//客户端请求连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//服务器端从编号为Sockid的Socket上接收客户连接请求
newsockid=accept(Sockid,Clientaddr, paddrlen)
发送/接收数据
//面向连接:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//面向无连接:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
释放套接字
int close(int fd);
区别
- socket()的参数不同
- UDP Server不需要调用listen和accept
- UDP收发数据用sendto/recvfrom函数
- TCP:地址信息在connect/accept时确定
- UDP:在sendto/recvfrom函数中每次均需指定地址信息(客户端调用connect之后,不需要每次指定)
- UDP:shutdown函数无效
socket插入内核hash表
根据服务器和客户端的行为不同,bind()和sendto()都会调用到get_port(),
也就是说,在bind()或sendto()调用时,socket才被插入到内核表中。
bind() 绑定地址
sys_bind -> sock->ops->bind -> inet_bind -> sk->sk_prot->get_port
sk->sk_prot是udp_prot,这里实际调用udp_v4_get_port()函数。
sendto() 发送到指定地址
sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg
由于创建的是udp socket,因此sock->ops指向inet_dgram_ops,sendmsg()实际调用inet_sendmsg()函数。该函数中的有如下语句:
if (!inet_sk(sk)->inet_num && inet_autobind(sk))
return -EAGAIN;
客户端在执行sendto()前仅仅执行了socket()操作,
此时inet_num=0,因此执行了inet_autobind(),
该函数会调用sk->sk_prot->get_port()。
从而回到了udp_v4_get_port()函数,它会将sk插入到内核表udp_table中。
UDP调用connect
标准的udp客户端开了套接口后,一般使用sendto和recvfrom函数来发数据,
udp发送数据有两种方法供大家选用的:
- socket–>sendto()或recvfrom()
- socket–>connect()–>send()或recv()
sendto和recvfrom在收发时指定地址,而send和recv则没有,地址是在connect指定的.
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
在udp编程中,如果你只往一个地址发送,那么你可以使用send和recv,
在使用它们之前用connect指定目的地址。connect函数在udp中就是这个作用,用它来检测udp端口的是否开放是没有用的。
udp中也有connect,只是它的connect不会进行三步握手,udp中调用connect时什么包也不发送。
调用connect是可选的,调用connect后就可以使用send、recv来进行UDP的收发包,而不必每次都要指定地址,
然后使用sendto、recvfrom进行操作,当然也可以调用sendto recvfrom。
没有调用connect那只能调用sendto、recvfrom,不可以调用send、recv。
调用sendto的时候第五个参数必须是NULL,第六个参数是0.
调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文.
UDP中使用connect可以提高效率.原因如下:
- 普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
- 采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询.
TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来.
UDP中可以多次调用connect,TCP只能调用一次connect. UDP多次调用connect有两种用途:
- 指定一个新的ip&port连结. 指定新连结,直接设置connect第二个参数即可.
- 断开和之前的ip&port的连结. 断开连结,需要将connect第二个参数中的sin_family设置成AF_UNSPEC即可.
UDP中使用connect的好处:
- 会提升效率
- 高并发服务中会增加系统稳定性
内核:发送时有两种调用方式:sys_send()和sys_sendto(),
两者的区别在于sys_sendto()需要给入目的地址的参数;
而sys_send()调用前需要调用sys_connect()来绑定目的地址信息;
两者的后续调用是相同的。如果调用sys_sendto()发送,
地址信息在sys_sendto()中从用户空间拷贝到内核空间,
而报文内容在udp_sendmsg()中从用户空间拷贝到内核空间。
TCP使用sendto
tcp_v4_connect函数代码片段:
/* 记录目的端口和目的IP */
inet->dport = usin->sin_port;
inet->daddr = daddr;
将目标地址及端口记录在inet中!
同样的,TCP也可以调用sendto、recvfrom来完成数据的读写。Linux内核中send系统调用:
SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
unsigned int, flags)
{
return sys_sendto(fd, buff, len, flags, NULL, 0);
}
可以看到,send直接调用sendto,后面两个参数直接填NULL,0,
因此对于TCP调用来将,使用sendto,并将后两个参数置0,一样可以工作。
TCP/UDP Send流程代码分析
TCP调用sendto时没有给定地址,如何来完成TCP传输?sendto函数原型:
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned, flags, struct sockaddr __user *, addr,
int, addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg;
struct iovec iov;
int fput_needed;
if (len > INT_MAX)
len = INT_MAX;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
iov.iov_base = buff;
iov.iov_len = len;
msg.msg_name = NULL;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, (struct sockaddr *)&address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg, len);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
tips: 使用sock->file->f_flags & O_NONBLOCK 来检查是否是nonblock的传送。
sendto中首先调用函数move_addr_to_kernel,将地址copy进内核空间:
int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr *kaddr)
{
if (ulen < 0 || ulen > sizeof(struct sockaddr_storage))
return -EINVAL;
if (ulen == 0)
return 0;
if (copy_from_user(kaddr, uaddr, ulen))
return -EFAULT;
return audit_sockaddr(ulen, kaddr);
}
对于TCP来说,传入参数分别为NULL,0,这部分不会执行到。
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
地址存入msg中,用于UDP连接。
然后调用sock_sendmsg来完成数据发送,调用顺序:
sys_send -> sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg -> inet_sendmsg -> sk->sk_prot->sendmsg
static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size)
{
struct sock_iocb *si = kiocb_to_siocb(iocb);
int err;
si->sock = sock;
si->scm = NULL;
si->msg = msg;
si->size = size;
err = security_socket_sendmsg(sock, msg, size);
if (err)
return err;
return sock->ops->sendmsg(iocb, sock, msg, size);
}
根据socket不同具体对应于tcp_sendmsg、udp_sendmsg
udp sendmsg
udp_sendmsg代码分析:
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len);
struct inet_sock *inet = inet_sk(sk);
if (msg->msg_name) {
struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
if (msg->msg_namelen < sizeof(*usin))
return -EINVAL;
if (usin->sin_family != AF_INET) {
if (usin->sin_family != AF_UNSPEC)
return -EAFNOSUPPORT;
}
daddr = usin->sin_addr.s_addr;
dport = usin->sin_port;
if (dport == 0)
return -EINVAL;
} else {
if (sk->sk_state != TCP_ESTABLISHED)
return -EDESTADDRREQ;
daddr = inet->daddr;
dport = inet->dport;
/* Open fast path for connected socket.
Route will not be used, if at least one option is set.
*/
connected = 1;
}
ipc.addr = inet->saddr;
这段代码获取要发送数据的目的地址和端口号。
一种情况是调用sendto()发送数据,此时目的的信息以参数传入,存储在msg->msg_name中,因此从中取出daddr和dport;
另一种情况是调用connect(), send()发送数据,在connect()调用时绑定了目的的信息,存储在inet中,
并且由于是调用了connect(),sk->sk_state会设置为TCP_ESTABLISHED。
以后调用send()发送数据时,无需要再给入目的信息参数,因此从inet中取出dadr和dport。而connected表示了该socket是否已绑定目的。
tcp sendmsg
tcp_sendmsg代码分析,调用流程:
tcp_sendmsg -> tcp_push -> __tcp_push_pending_frames -> tcp_write_xmit -> tcp_transmit_skb
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
struct inet_sock *inet;
.
.
inet = inet_sk(sk);
/* Build TCP header and checksum it. */
th = tcp_hdr(skb);
th->source = inet->sport;
th->dest = inet->dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) |
tcb->flags);
.
.
}
tcp_sendmsg目标地址在函数tcp_transmit_skb中获取,获取的值是调用connect时存储在inet中!