基于TCP的socket编程
TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器调用socket()
、bind()
、listen()
完成初始化后,调用accept()
阻塞等待,处于监听端口的状态,客户端调用socket()
初始化后,调用connect()
发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()
返回,同时应答一个ACK段,服务器收到后从accept()
返回。
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()
返回后立刻调用read()
,读socket
就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()
发送请求给服务器,服务器收到后从read()
返回,对客户端的请求进行处理,在此期间客户端调用read()
阻塞等待服务器的应答,服务器调用write()
将处理结果发回给客户端,再次调用read()
阻塞等待下一条请求,客户端收到后从read()
返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()
关闭连接,就像写端关闭的管道一样,服务器的read()
返回0
,这样服务器就知道客户端关闭了连接,也调用close()
关闭连接。注意,任何一方调用close()
后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()
则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()
会发出SYN段 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()
返回0
就表明收到了FIN段
最简单的C/S实例
下面通过最简单的客户端/服务器程序的实例来学习socket API。
server.c
的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char* argv[]){
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(connfd, buf, n);
close(connfd);
}
return 0;
}
client.c
的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys / socket.h>
#include <netinet / in.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[]){
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2) {
fputs("usage: ./client message\n", stderr);
return -1;
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
n = read(sockfd, buf, MAXLINE);
printf("Response from server:\n");
write(STDOUT_FILENO, buf, n);
close(sockfd);
return 0;
}
由于客户端不需要固定的端口号,因此不必调用bind()
,客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind()
,只是没有必要调用bind()
固定一个端口号,服务器也不是必须调用bind()
,但如果服务器不调用bind()
,内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
客户端和服务器启动后可以使用netstat
命令查看链接情况:
netstat -apn|grep 6666
出错处理封装函数
上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
函数封装思想
结合man-page和errno进行封装。在封装的时候起名可以把第一个函数名的字母大写,如socket
可以封装成Socket
,这样可以按shift+k
进行搜索,shift+k
搜索函数说明的时候不区分大小写,使用man page也可以查看,man page对大小写不区分。
像accept
、read
这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高,会优先处理信号;信号处理完成后,会使accept
或者read
解除阻塞,然后返回,此时返回值为 -1
,设置errno=EINTR
;
errno=ECONNABORTED
表示连接被打断,异常。
errno宏
在/usr/include/asm-generic/errno.h
文件中包含了errno所有的宏和对应的错误描述信息。
粘包的概念
粘包: 多次数据发送,首尾相连,接收端接收的时候不能正确区分第一次发送多少,第二次发送多少。
粘包问题分析和解决??
- 方案1: 包头+数据
- 如4位的数据长度+数据 -----------> 00101234567890
- 其中0010表示数据长度, 1234567890表示10个字节长度的数据
- 另外,发送端和接收端可以协商更为复杂的报文结构,这个报文结构就相当于双方约定的一个协议
- 方案2: 添加结尾标记
- 如结尾最后一个字符为\n $等
- 方案3: 数据包定长
- 如发送方和接收方约定,每次只发送128个字节的内容,接收方接收定长128个字节就可以了
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c
:
wrap.c代码解读和分析
要求能看懂代码。会使用即可
wrap.c
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s){
perror(s);
return -1;
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen){
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen){
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog){
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol){
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes){
ssize_t n;
again:
if ((n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes){
ssize_t n;
again:
if ((n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd){
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
ssize_t Readn(int fd, void *vptr, size_t n){
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n){
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr){
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen){
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ((rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
基于TCP的字母大小写转换
自己分别编写服务端和客户端程序,实现两个程序之间的通信。服务端将客户端发来的字母转换为大写字母返回给客户端
server.c
#include <arpa/inet.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
// 1.创建socket通信文件描述符
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 1) {
perror("socket error");
return -1;
}
// 2.绑定IP地址及端口
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &myaddr.sin_addr.s_addr);
int retbind = bind(sfd, (struct sockaddr *) &myaddr, sizeof(myaddr));
if (retbind < 0) {
perror("bind error");
return -1;
}
// 3.监听socket通信文件描述符有没有客户端进行连接
listen(sfd, 20);
struct sockaddr_in othaddr;
bzero(&othaddr, sizeof(othaddr));
int othlen = sizeof(othaddr);
// 4.接收客户端的连接,并且生成通信文件描述符
int afd = accept(sfd, (struct sockaddr *) &othaddr, &othlen);
if (afd < 0) {
perror("accept error");
return -1;
}
// 5.对接收到或者要发送的数据进行处理
char buf[1024];
int ret = 1;
int i = 0;
bzero(&buf, sizeof(buf));
while (1) {
ret = read(afd, buf, sizeof(buf));
if (ret <= 0) {
printf("read error or client closed. ret=[%d]\n", ret);
break;
}
printf("ret=[%d],buf=[%s]\n", ret, buf);
for (i = 0; i < ret; i++) {
buf[i] = toupper(buf[i]);
}
write(afd, buf, ret);
}
// 6.通信完成后关闭使用过的文件描述符
close(sfd);
close(afd);
return 0;
}
client.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
// 1.创建socket通信文件描述符
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd < 0) {
perror("socket error");
return -1;
}
// 2.连接服务端
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
clientaddr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &clientaddr.sin_addr);
//int len = sizeof(clientaddr);
int cret = connect(cfd, (struct sockaddr *) &clientaddr, sizeof(clientaddr));
if (cret < 0) {
perror("socket error");
return -1;
}
// 3.对接收到或要发送到数据进行处理
char buf[1024];
int ret = 1;
while (1) {
bzero(&buf, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf));
write(cfd, buf, ret);
bzero(&buf, sizeof(buf));
ret = read(cfd, buf, sizeof(buf));
if (ret <= 0) {
printf("read error or server closed. ret=[%d]\n", ret);
break;
}
printf("ret=[%d],buf=[%s]\n", ret, buf);
}
// 4.关闭使用过的文件描述符
close(cfd);
return 0;
}
让服务端无视TIME_WAIT
使用setsockopt
函数在服务端bind之前修改socket的属性,
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
函数参数
sockfd
:socket返回的文件描述符level
:固定设置为SOL_SOCKET
optname
:要修改哪个属性,可以使用man 7 socket
查看支持修改的属性,这里固定设置为SO_REUSEADDR
optval
:要修改成什么样,1为真,0为假。这里设置为真optlen
:optval
的大小,这里直接sizeof(int)
即可
返回值
- 成功:
0
- 失败:
-1
,并设置errno
所以在bind之前加入这几句代码即可
int optval = 1;
// int ret = setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int ret = setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));
if (ret == -1){
perror("setsockopt");
return -1;
}